示例#1
0
        public async Task ExampleUsage2()
        {
            var t = Log.MethodEntered("DataStoreExample2.ExampleUsage2");

            // Add a thunk middleware to allow dispatching async actions:
            var thunkMiddleware = Middlewares.NewThunkMiddleware <MyAppState1>();

            // aDD A logging middleware to log all dispatched actions:
            var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>();

            // Add a recorder middleware to enable hot reload by replaying previously recorded actions:
            var recorder      = new ReplayRecorder <MyAppState1>();
            var recMiddleware = recorder.CreateMiddleware();

            var undoable = new UndoRedoReducer <MyAppState1>();
            // To allow undo redo on the full store wrap the main reducer with the undo reducer:
            var undoReducer = undoable.Wrap(MyReducers1.ReduceMyAppState1);

            var data  = new MyAppState1(null, null, 0); // the initial immutable state
            var store = new DataStore <MyAppState1>(undoReducer, data, loggingMiddleware, recMiddleware, thunkMiddleware);

            store.storeName = "Store 1";

            TestNormalDispatchOfActions(store);

            TestUndoAndRedo(store);

            await TestAsyncActions(store);

            await TestReplayRecorder(recorder, store);

            TestSkippableUndoActions(store);

            Log.MethodDone(t);
        }
示例#2
0
        private static class MyReducers1 { // The reducers to modify the immutable datamodel:
            // The most outer reducer is public to be passed into the store:
            public static MyAppState1 ReduceMyAppState1(MyAppState1 model, object action)
            {
                bool changed = false;

                model.MutateField(model.user, action, (_, user, a) => ReduceUser(user, a), ref changed);
                if (changed)
                {
                    model.MarkMutated();
                }
                return(model);
            }
示例#3
0
        private static class MyReducers1 { // The reducers to modify the immutable datamodel:
            // The most outer reducer is public to be passed into the store:
            public static MyAppState1 ReduceMyAppState1(MyAppState1 previousState, object action)
            {
                bool changed = false;
                var  newUser = previousState.user.Mutate(action, ReduceUser, ref changed);

                if (changed)
                {
                    return(new MyAppState1(newUser));
                }
                return(previousState);
            }
示例#4
0
        private static class MyReducers1 { // The reducers to modify the immutable datamodel:
            // The most outer reducer is public to be passed into the store:
            public static MyAppState1 ReduceMyAppState1(MyAppState1 myState, object action)
            {
                bool changed = false;
                var  users   = myState.users.Mutate(action, ReduceUsers, ref changed);
                var  someIds = myState.someUuids.Mutate(action, ReduceSomeIds, ref changed);

                if (changed)
                {
                    return(new MyAppState1(users, someIds));
                }
                return(myState);
            }
示例#5
0
        private MyAppState1 ReduceMyAppState1(MyAppState1 previousState, object action)
        {
            bool changed = false;
            var  newA    = previousState.substateA.Mutate(action, SubStateAReducer, ref changed);
            var  newB    = previousState.substateB.Mutate(action, SubStateBReducer, ref changed);

            if (changed)
            {
                return(new MyAppState1(newA, newB));
            }
            return(previousState);
        }
示例#6
0
        public void ExampleUsage1()
        {
            var t = Log.MethodEntered("DataStoreExample1.ExampleUsage1");

            var data  = new MyAppState1(); // the initial immutable state
            var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, data);

            // Register a listener that listens to a subtree of the complete state tree:
            var firstContactWasModifiedCounter = 0;

            // Listen to state changes of the first contact of the main user:
            store.AddStateChangeListener(state => state.user?.contacts?.FirstOrDefault(), (firstContact) => {
                firstContactWasModifiedCounter++;
            });
            Assert.Equal(1, firstContactWasModifiedCounter);

            store.Dispatch(new ActionLoginUser()
            {
                newLoggedInUser = new MyUser1("Karl")
            });
            Assert.NotNull(store.GetState().user);

            store.Dispatch(new ActionOnUser.ChangeAge()
            {
                targetUser = "******", newAge = 99
            });
            Assert.Equal(99, store.GetState().user.age);

            store.Dispatch(new ActionOnUser.AddContact()
            {
                targetUser = "******", newContact = new MyUser1(name: "Tim")
            });
            Assert.Equal("Tim", store.GetState().user.contacts.First().name);
            Assert.Equal(2, firstContactWasModifiedCounter);

            // Change name of Tim to Peter:
            store.Dispatch(new ActionOnUser.ChangeName()
            {
                targetUser = "******", newName = "Peter"
            });
            Assert.Equal("Peter", store.GetState().user.contacts.First().name);
            Assert.Equal(3, firstContactWasModifiedCounter);

            store.Dispatch(new ActionLogoutUser());
            Assert.Null(store.GetState().user);

            Log.MethodDone(t);
        }
示例#7
0
        private static class MyReducers1 { // The reducers to modify the immutable datamodel:
            // The most outer reducer is public to be passed into the store:
            public static MyAppState1 ReduceMyAppState1(MyAppState1 previousState, object action)
            {
                bool changed = false;

                if (action is ResetStoreAction)
                {
                    return(new MyAppState1());
                }
                var newWeather = previousState.currentWeather.Mutate(action, ReduceWeather, ref changed);
                var newUser    = previousState.user.Mutate(action, ReduceUser, ref changed);

                if (changed)
                {
                    return(new MyAppState1(newUser, newWeather));
                }
                return(previousState);
            }
示例#8
0
        private async Task TestReplayRecorderOnNewStore(ReplayRecorder <MyAppState1> recorder, MyAppState1 finalStateOfFirstStore)
        {
            var t = Log.MethodEntered("TestReplayRecorderOnNewStore");

            // Connect the recorder to the new store:
            var recMiddleware = recorder.CreateMiddleware();
            var undoable      = new UndoRedoReducer <MyAppState1>();
            var logging       = Middlewares.NewLoggingMiddleware <MyAppState1>();

            var data2  = new MyAppState1(null, null, 0);
            var store2 = new DataStore <MyAppState1>(undoable.Wrap(MyReducers1.ReduceMyAppState1), data2, logging, recMiddleware);

            store2.storeName = "Store 2";

            // Replaying the recorder will now fill the second store with the same actions:
            await recorder.ReplayStore();

            AssertEqualJson(finalStateOfFirstStore, store2.GetState());

            Log.MethodDone(t);
        }
示例#9
0
        public void ExampleUsage1()
        {
            var t = Log.MethodEntered("DataStoreExample3.ExampleUsage1");

            // A middleware that will allow to use mutable data in the data store:
            var mutableMiddleware = Middlewares.NewMutableDataSupport <MyAppState1>();

            // Add A logging middleware to log all dispatched actions:
            var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>();

            MyUser1 user = new MyUser1()
            {
                name = "Carl"
            };
            var model = new MyAppState1()
            {
                user = user
            };

            var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, model, loggingMiddleware, mutableMiddleware);

            IoC.inject.SetSingleton(store);
            store.storeName = "Store 4";

            // Setup 3 listeners that react when the name of the user or his contacts change:
            var userChangedCounter         = 0;
            var userNameChangedCounter     = 0;
            var contact1NameChangedCounter = 0;
            var contact2NameChangedCounter = 0;

            store.AddStateChangeListener(s => s.user, (MyUser1 theChangedUser) => {
                userChangedCounter++;
            });
            store.AddStateChangeListener(s => s.user?.name, (string theChangedName) => {
                userNameChangedCounter++;
            });
            store.AddStateChangeListener(s => s.user?.contacts?.FirstOrDefault()?.contactData.name, (_) => {
                contact1NameChangedCounter++;
            });
            store.AddStateChangeListener(s => s.user?.contacts?.Skip(1).FirstOrDefault()?.contactData.name, (_) => {
                contact2NameChangedCounter++;
            });

            var contact1 = new MyUser1()
            {
                name = "Tom"
            };
            { // Add a first contact to the user:
                Assert.Null(user.contacts);
                store.Dispatch(new ActionAddContact()
                {
                    targetUserId = user.id,
                    newContact   = new MyContact1()
                    {
                        contactData = contact1
                    }
                });
                Assert.Same(contact1, user.contacts.First().contactData);
                Assert.True(user.WasModifiedInLastDispatch());

                // Now that there is a contact 1 the listener was triggered:
                Assert.Equal(1, userChangedCounter);
                Assert.Equal(0, userNameChangedCounter);
                Assert.Equal(1, contact1NameChangedCounter);
                Assert.Equal(0, contact2NameChangedCounter);
            }

            var contact2 = new MyUser1()
            {
                name = "Bill"
            };
            { // Add a second contact to the user which should not affect contact 1:
                store.Dispatch(new ActionAddContact()
                {
                    targetUserId = user.id,
                    newContact   = new MyContact1()
                    {
                        contactData = contact2
                    }
                });
                Assert.Same(contact2, user.contacts.Last().contactData);
                Assert.True(user.WasModifiedInLastDispatch());
                Assert.False(contact1.WasModifiedInLastDispatch());

                Assert.Equal(2, userChangedCounter);
                Assert.Equal(0, userNameChangedCounter);
                Assert.Equal(1, contact1NameChangedCounter);
                Assert.Equal(1, contact2NameChangedCounter);
            }
            { // Change the name of contact 1 which should not affect contact 2:
                var newName1 = "Toooom";
                store.Dispatch(new ActionChangeUserName()
                {
                    targetUserId = contact1.id, newName = newName1
                });
                Assert.True(user.WasModifiedInLastDispatch());
                Assert.True(contact1.WasModifiedInLastDispatch());
                Assert.False(contact2.WasModifiedInLastDispatch());

                Assert.Equal(3, userChangedCounter);
                Assert.Equal(0, userNameChangedCounter);
                Assert.Equal(2, contact1NameChangedCounter);
                Assert.Equal(1, contact2NameChangedCounter);
            }
            { // Change the name of the user which should not affect the 2 contacts:
                var newName = "Caaaarl";
                Assert.NotEqual(newName, user.name);
                Assert.Equal(user.name, store.GetState().user.name);
                var tBeforeDispatch = user.LastMutation;
                store.Dispatch(new ActionChangeUserName()
                {
                    targetUserId = user.id, newName = newName
                });
                Assert.Equal(newName, store.GetState().user.name);
                Assert.Equal(newName, user.name);
                Assert.Same(model, store.GetState());

                Assert.NotEqual(tBeforeDispatch, user.LastMutation);
                Assert.True(user.WasModifiedInLastDispatch());
                Assert.False(contact1.WasModifiedInLastDispatch());
                Assert.False(contact2.WasModifiedInLastDispatch());

                Assert.Equal(4, userChangedCounter);
                Assert.Equal(1, userNameChangedCounter);
                Assert.Equal(2, contact1NameChangedCounter);
                Assert.Equal(1, contact2NameChangedCounter);
            }
            { // Marking an object mutated while not dispatching will throw an exception:
                Assert.Throws <InvalidOperationException>(() => {
                    user.name = "Cooorl";
                    user.MarkMutated();
                });
                Assert.Equal(4, userChangedCounter); // Count should not have changed
                Assert.Equal(1, userNameChangedCounter);
            }
        }
示例#10
0
        public async Task ExampleUsage1()
        {
            var t = Log.MethodEntered("DataStoreExample3.ExampleUsage1");

            // Add a thunk middleware to allow dispatching async actions:
            var thunkMiddleware = Middlewares.NewThunkMiddleware <MyAppState1>();

            // aDD A logging middleware to log all dispatched actions:
            var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>();

            var serverOutboxHandler = new ServerOutboxHandler <MyAppState1>();
            // To allow undo redo on the full store wrap the main reducer with the undo reducer:
            var outboxReducer = serverOutboxHandler.Wrap(MyReducers1.ReduceMyAppState1);
            var initialState  = new MyAppState1(); // the initial immutable state
            var store         = new DataStore <MyAppState1>(outboxReducer, initialState, loggingMiddleware, thunkMiddleware);

            IoC.inject.SetSingleton(store);
            store.storeName = "Store 3";

            { // Do a login which is an async server action that cant be cached optimistically and wont work offline:
                Func <Task> asyncLoginTask = async() => {
                    await TaskV2.Delay(100);

                    store.Dispatch(new ActionUserLoggedIn()
                    {
                        newLoggedInUser = new MyUser1("*****@*****.**")
                    });
                };
                await(store.Dispatch(asyncLoginTask) as Task);
            }
            { // Change the email a first time:
                var a = new ActionOnUser.ChangeEmail()
                {
                    targetEmail = "*****@*****.**", newEmail = "*****@*****.**"
                };
                store.Dispatch(a);
                Assert.Equal(a, store.GetState().serverOutbox.serverActions.First());
                Assert.False(store.GetState().user.emailConfirmed);
            }
            { // Change the email a second time:
                var a = new ActionOnUser.ChangeEmail()
                {
                    targetEmail = "*****@*****.**", newEmail = "*****@*****.**"
                };
                store.Dispatch(a);
                Assert.Equal(a, store.GetState().serverOutbox.serverActions.Last());
            }

            Assert.Equal(2, store.GetState().serverOutbox.serverActions.Count);
            await store.SyncWithServer(store.GetState().serverOutbox.serverActions.First());

            Assert.Single(store.GetState().serverOutbox.serverActions);
            await store.SyncWithServer(store.GetState().serverOutbox.serverActions.First());

            Assert.Empty(store.GetState().serverOutbox.serverActions);
            Assert.True(store.GetState().user.emailConfirmed);

            { // Simulate a server task that has a timeout:
                var a = new ActionOnUser.ChangeEmail()
                {
                    targetEmail = "*****@*****.**", newEmail = "*****@*****.**", simulateOneTimeout = true
                };
                store.Dispatch(a);
                Assert.Single(store.GetState().serverOutbox.serverActions);
                Assert.False(store.GetState().user.emailConfirmed);
                await store.SyncWithServer(a);

                Assert.Empty(store.GetState().serverOutbox.serverActions);
                Assert.Equal(2, a.sentToServerCounter);
                Assert.True(store.GetState().user.emailConfirmed);
            }
            { // Simulate the server rejecting an email change:
                var a = new ActionOnUser.ChangeEmail()
                {
                    targetEmail = "*****@*****.**", newEmail = "*****@*****.**", simulateError = true
                };
                store.Dispatch(a);
                await store.SyncWithServer(a);

                Assert.Empty(store.GetState().serverOutbox.serverActions);
                Assert.Equal("*****@*****.**", store.GetState().user.email);
                Assert.True(store.GetState().user.emailConfirmed);
            }
            { // Test persisting and restoring the full store and continue with the pending server requests:
                store.Dispatch(new ActionOnUser.ChangeEmail()
                {
                    targetEmail = "*****@*****.**", newEmail = "*****@*****.**"
                });
                store.Dispatch(new ActionOnUser.ChangeEmail()
                {
                    targetEmail = "*****@*****.**", newEmail = "*****@*****.**"
                });
                Assert.Equal(2, store.GetState().serverOutbox.serverActions.Count);
                Assert.False(store.GetState().user.emailConfirmed);
                Assert.Equal("*****@*****.**", store.GetState().user.email);

                // Simulate persisiting the store to disk and back into memory:
                string persistedStateJson = TypedJsonHelper.NewTypedJsonWriter().Write(store.GetState());

                store.Destroy(); // Destroy the old store before loading the state again into an new store
                var data2  = TypedJsonHelper.NewTypedJsonReader().Read <MyAppState1>(persistedStateJson);
                var store2 = new DataStore <MyAppState1>(outboxReducer, data2, loggingMiddleware, thunkMiddleware);
                IoC.inject.SetSingleton(store2, overrideExisting: true);
                store2.storeName = "Store 3 (2)";

                Assert.Equal(2, store2.GetState().serverOutbox.serverActions.Count);
                Assert.False(store2.GetState().user.emailConfirmed);
                Assert.Equal("*****@*****.**", store2.GetState().user.email);

                // Sync the pending server tasks one after another:
                foreach (var serverAction in store2.GetState().serverOutbox.serverActions)
                {
                    await store2.SyncWithServer(serverAction);
                }
                Assert.True(store2.GetState().user.emailConfirmed);
                Assert.NotNull(store2.GetState().serverOutbox);
                Assert.Empty(store2.GetState().serverOutbox.serverActions);
            }
        }
示例#11
0
        public void StressTest1()
        {
            var initialState = new MyAppState1(new SubStateA("a1"), new SubStateB("b1"), NewVeryLargeList());
            var store        = new DataStore <MyAppState1>(ReduceMyAppState1, initialState);

            // In total this test will create 4 million state change listeners:
            int         listenerCount = 100000;
            StopwatchV2 t1, t2, t3, t4; // The 4 measured timings of the Dispatches

            {                           // Add subListeners that are only informed by the one listener attached directly to the store:
                var counterA1     = 0;
                var subListenersA = store.NewSubStateListener(s => s.substateA);
                for (int i = 0; i < listenerCount; i++)
                {
                    subListenersA.AddStateChangeListener(substateA => substateA.valA, newValA => counterA1++);
                }
                t1 = Log.MethodEntered("ActionChangeSubstateA");
                store.Dispatch(new ActionChangeSubstateA()
                {
                    newVal = "a2"
                });
                Log.MethodDone(t1);
                Assert.Equal("a2", store.GetState().substateA.valA);
                Assert.Equal(listenerCount, counterA1);
            }
            { // Now add additional listeners to check if it makes Dispatching slower:
                var counterB1     = 0;
                var subListenersB = store.NewSubStateListener(s => s.substateB);
                for (int i = 0; i < listenerCount; i++)
                {
                    subListenersB.AddStateChangeListener(substateB => substateB.valB, newValB => counterB1++);
                }
                t2 = Log.MethodEntered("ActionChangeSubstateB");
                store.Dispatch(new ActionChangeSubstateB()
                {
                    newVal = "b2"
                });
                Log.MethodDone(t2);
                Assert.Equal("b2", store.GetState().substateB.valB);
                Assert.Equal(listenerCount, counterB1);

                // Make sure the additional listeners did not make the Dispatch slower:
                float t1t2Ratio = (float)t1.ElapsedMilliseconds / (float)t2.ElapsedMilliseconds;
                Log.d("t1t2Ratio=" + t1t2Ratio);
                Assert.True(0.3f < t1t2Ratio && t1t2Ratio < 3f, "t1t2Ratio=" + t1t2Ratio);
            }
            { // Now add the listeners directly to the store which slows down the Dispatches:
                var counterA2 = 0;
                var counterB2 = 0;
                for (int i = 0; i < listenerCount; i++)
                {
                    store.AddStateChangeListener(s => s.substateA, newSubA => { counterA2++; });
                    store.AddStateChangeListener(s => s.substateB, newSubB => { counterB2++; });
                }

                t3 = Log.MethodEntered("ActionChangeSubstateA2");
                store.Dispatch(new ActionChangeSubstateA()
                {
                    newVal = "a3"
                });
                Log.MethodDone(t3);
                Assert.Equal("a3", store.GetState().substateA.valA);
                Assert.Equal(listenerCount, counterA2);

                t4 = Log.MethodEntered("ActionChangeSubstateB2");
                store.Dispatch(new ActionChangeSubstateB()
                {
                    newVal = "b3"
                });
                Log.MethodDone(t4);
                Assert.Equal("b3", store.GetState().substateB.valB);
                Assert.Equal(listenerCount, counterB2);

                // Make sure the additional listeners make Dispatching much slower:
                float t1t3Ratio = (float)t3.ElapsedMilliseconds / (float)t1.ElapsedMilliseconds;
                Log.d("t1t3Ratio=" + t1t3Ratio);
                Assert.True(1.5f < t1t3Ratio, "t1t3Ratio=" + t1t3Ratio);

                float t1t4Ratio = (float)t4.ElapsedMilliseconds / (float)t1.ElapsedMilliseconds;
                Log.d("t1t4Ratio=" + t1t4Ratio);
                Assert.True(2f < t1t4Ratio, "t1t4Ratio=" + t1t4Ratio);

                float t3t4Ratio = (float)t3.ElapsedMilliseconds / (float)t4.ElapsedMilliseconds;
                Log.d("t3t4Ratio=" + t3t4Ratio);
                Assert.True(0.5f < t3t4Ratio && t3t4Ratio < 2f, "t3t4Ratio=" + t3t4Ratio);
            }

            TestListEntrySelector(store);
        }
示例#12
0
        public void ExampleUsage1()
        {
            var t = Log.MethodEntered("DataStoreExample1.ExampleUsage1");

            // Some initial state of the model (eg loaded from file when the app is started) is restored and put into the store:
            MyUser1 carl = new MyUser1(GuidV2.NewGuid(), "Carl", 99, null, MyUser1.MyEnum.State1);
            var     data = new MyAppState1(ImmutableDictionary <Guid, MyUser1> .Empty.Add(carl.id, carl), null);

            var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, data);

            var keepListenerAlive   = true;
            var usersChangedCounter = 0;

            store.AddStateChangeListener(state => state.users, (changedUsers) => {
                usersChangedCounter++;
                return(keepListenerAlive);
            }, triggerInstantToInit: false);

            ActionAddSomeId a1 = new ActionAddSomeId()
            {
                someId = GuidV2.NewGuid()
            };

            store.Dispatch(a1);
            Assert.Equal(0, usersChangedCounter); // no change happened in the users
            Assert.Equal(a1.someId, store.GetState().someUuids.Value.Single());

            MyUser1 carlsFriend = new MyUser1(GuidV2.NewGuid(), "Carls Friend", 50, null, MyUser1.MyEnum.State1);

            store.Dispatch(new ActionOnUser.AddContact()
            {
                targetUser = carl.id, newContact = carlsFriend
            });
            Assert.Equal(1, usersChangedCounter);
            Assert.Equal(carlsFriend, store.GetState().users[carlsFriend.id]);
            Assert.Contains(carlsFriend.id, store.GetState().users[carl.id].contacts);

            store.Dispatch(new ActionOnUser.ChangeName()
            {
                targetUser = carl.id, newName = "Karl"
            });
            Assert.Equal(2, usersChangedCounter);
            Assert.Equal("Karl", store.GetState().users[carl.id].name);


            store.Dispatch(new ActionOnUser.ChangeAge()
            {
                targetUser = carlsFriend.id, newAge = null
            });
            Assert.Equal(3, usersChangedCounter);
            Assert.Null(store.GetState().users[carlsFriend.id].age);

            Assert.NotEqual(MyUser1.MyEnum.State2, store.GetState().users[carl.id].myEnum);
            store.Dispatch(new ActionOnUser.ChangeEnumState()
            {
                targetUser = carl.id, newEnumValue = MyUser1.MyEnum.State2
            });
            Assert.Equal(MyUser1.MyEnum.State2, store.GetState().users[carl.id].myEnum);

            // Remove the listener from the store the next time an action is dispatched:
            keepListenerAlive = false;
            Assert.Equal(4, usersChangedCounter);
            store.Dispatch(new ActionOnUser.ChangeAge()
            {
                targetUser = carlsFriend.id, newAge = 22
            });
            Assert.Equal(5, usersChangedCounter);
            // Test that now the listener is removed and will not receive any updates anymore:
            store.Dispatch(new ActionOnUser.ChangeAge()
            {
                targetUser = carlsFriend.id, newAge = 33
            });
            Assert.Equal(5, usersChangedCounter);

            Log.MethodDone(t);
        }