private static ImmutableList <MyUser1> ReduceContacts(MyUser1 user, ImmutableList <MyUser1> contacts, object action) { contacts = contacts.MutateEntries(action, ReduceUser); if (action is ActionOnUser.AddContact a && a.IsTargetUser(user)) { return(ImmutableExtensions.AddOrCreate <MyUser1>(contacts, (MyUser1)a.newContact)); } return(contacts); }
/// <summary> This method tests the <see cref="SkipInUndoRedoLogic"/> interface that can be added to /// any Action (see eg the <see cref="ActionUpdateDownloadProgress"/> Action below). </summary> private void TestSkippableUndoActions(DataStore <MyAppState1> store) { using var t = Log.MethodEntered(); var listenerWasCalledCounter = 0; store.AddStateChangeListener(x => x.downloadProgress, (int newDownloadPercentage) => { listenerWasCalledCounter++; }, triggerInstantToInit: false); store.Dispatch(new ActionLogoutUser()); Assert.Null(store.GetState().user); var newUser = new MyUser1("Karl"); store.Dispatch(new ActionLoginUser() { newLoggedInUser = newUser }); Assert.Same(newUser, store.GetState().user); { // Now a download progress is simulated Assert.Equal(0, listenerWasCalledCounter); Assert.Equal(0, store.GetState().downloadProgress); store.Dispatch(new ActionUpdateDownloadProgress(10)); Assert.Equal(10, store.GetState().downloadProgress); store.Dispatch(new ActionUpdateDownloadProgress(99)); Assert.Equal(99, store.GetState().downloadProgress); store.Dispatch(new ActionUpdateDownloadProgress(100)); Assert.Equal(100, store.GetState().downloadProgress); Assert.Equal(3, listenerWasCalledCounter); } // The download progress is not something that should be undoable, so // triggering an undo action now should directly skip all these // download progress events and undo the login Action directly: Assert.Same(newUser, store.GetState().user); store.Dispatch(new UndoAction <MyAppState1>()); Assert.Null(store.GetState().user); Assert.Equal(0, store.GetState().downloadProgress); Assert.Equal(4, listenerWasCalledCounter); store.Dispatch(new RedoAction <MyAppState1>()); Assert.Same(newUser, store.GetState().user); Assert.Equal(100, store.GetState().downloadProgress); Assert.Equal(5, listenerWasCalledCounter); }
private static MyUser1 ReduceUser(MyUser1 user, object action) { bool changed = false; var name = user.name.Mutate(action, ReduceUserName, ref changed); var age = user.age.Mutate(action, ReduceUserAge, ref changed); var contacts = user.contacts.Mutate(action, ReduceContacts, ref changed); var myEnum = user.myEnum.Mutate(action, ReduceMyEnum, ref changed); if (changed) { // user.id can never change so always take it over from last state: user = new MyUser1(user.id, name, age, contacts, myEnum); } return(user); }
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 MyUser1 ReduceUser(MyUser1 user, object action) { if (action is ActionLoginUser a1) { return(a1.newLoggedInUser); } // Now the default pattern to call .Mutate on all fields and check via a changed flag if any // of the field were changed which would require a new instance of the object to be created: bool changed = false; // Will be set to true by the .Mutate method if any field changes: var newName = user?.name.Mutate(action, ReduceUserName, ref changed); if (changed) { return(new MyUser1(newName)); } return(user); // None of the fields changed, old user can be returned }
private static MyUser1 ReduceUser(MyUser1 user, object action) { var changed = false; if (action is ActionChangeUserName a) { if (a.targetUserId == user.id) { user.name = a.newName; user.MarkMutated(); changed = true; } } user.contacts = user.MutateField(user.contacts, action, ContactsReducer, ref changed); if (changed) { user.MarkMutated(); } return(user); }
private static List <MyContact1> ContactsReducer(MyUser1 parent, List <MyContact1> contacts, object action) { var changed = false; contacts.MutateEntries(action, ContactReducer, ref changed); if (action is ActionAddContact c) { if (c.targetUserId == parent.id) { if (contacts == null) { contacts = new List <MyContact1>(); } contacts.Add(c.newContact); changed = true; } } if (changed) { parent.MarkMutated(); } return(contacts); }
private static MyUser1 ReduceUser(MyUser1 user, object action) { if (action is ActionLogoutUser) { return(null); } if (action is ActionUserLoggedIn a1) { return(a1.newLoggedInUser); } if (action is ActionOnUser a) { bool userChanged = false; // Will be set to true by the .Mutate method if any field changes: var isTargetUser = a.IsTargetUser(user); var email = user.email.Mutate(isTargetUser, a, ReduceUserEmail, ref userChanged); var emailConfirmed = user.emailConfirmed.Mutate(isTargetUser, a, ReduceEmailConfirmed, ref userChanged); if (userChanged) { return(new MyUser1(email, emailConfirmed)); } } return(user); // None of the fields changed, old user can be returned }
private static MyUser1 ReduceUser(MyUser1 user, object action) { if (action is ActionLogoutUser) { return(null); } if (action is ActionLoginUser a1) { return(a1.newLoggedInUser); } if (action is ActionOnUser a) { bool userChanged = false; var isTargetUser = a.IsTargetUser(user); var name = user.name.Mutate(isTargetUser, a, ReduceUserName, ref userChanged); var age = user.age.Mutate(isTargetUser, a, ReduceUserAge, ref userChanged); var contacts = user.MutateField(user.contacts, a, ReduceContacts, ref userChanged); if (userChanged) { return(new MyUser1(name, age, contacts)); } } return(user); // None of the fields changed, old user can be returned }
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); } }
public bool IsTargetUser(MyUser1 user) { return(user.email == targetEmail); }
public MyAppState1(MyUser1 user = null) { this.user = user; }
public bool IsTargetUser(MyUser1 user) { return(user.name == targetUser); }
public MyAppState1(MyUser1 user, List <string> currentWeather, int downloadProgress) { this.user = user; this.currentWeather = currentWeather; this.downloadProgress = downloadProgress; }
public MyAppState1(MyUser1 user = null, List <string> currentWeather = null) { this.user = user; this.currentWeather = currentWeather; }
public async Task RunTestTask() { MyUser1 initialState = null; // Initially no user is logged in var store = new DataStore <MyUser1>(MyReducers1.ReduceUser, initialState, Middlewares.NewLoggingMiddleware <MyUser1>()); var links = gameObject.GetLinkMap(); var loginButton = links.Get <Button>("LoginBtn"); var loginButtonWasClicked = loginButton.SetOnClickAction(async delegate { loginButton.gameObject.Destroy(); await TaskV2.Run(async() => { await TaskV2.Delay(1000); store.Dispatch(new ActionLoginUser() { newLoggedInUser = new MyUser1("Karl") }); }).LogOnError(); Assert.IsNotNull(store.GetState()); }); // Register a listener that is attached to the UI button to demonstrate that its no longer triggered once the button is destroyed: var userNameChangedCounter = 0; var subStateListener = store.NewSubStateListenerForUnity(loginButton, user => user); subStateListener.AddStateChangeListener(x => x?.name, newName => { userNameChangedCounter++; Toast.Show("User name changed to " + newName); Assert.IsFalse(loginButton.IsDestroyed()); }, triggerInstantToInit: false); var userInfoText1 = links.Get <InputField>("UserNameInput1"); ConnectInputFieldUiToModel(userInfoText1, store); var userInfoText2 = links.Get <InputField>("UserNameInput2"); ConnectInputFieldUiToModel(userInfoText2, store); var oldCounterValue = userNameChangedCounter; SimulateButtonClickOn("LoginBtn"); await loginButtonWasClicked; Assert.IsTrue(loginButton.IsDestroyed()); // Since the button was destroyed, the counter should not change anymore: Assert.AreEqual(oldCounterValue, userNameChangedCounter); Toast.Show("Changing user name from background thread..."); await Task.Delay(2000); // When NewSubStateListener instead of NewSubStateListenerForUnity is used, the // event will arrive on the thread where it was dispatched: var wasCalledOnMainThread = true; store.NewSubStateListener(user => user).AddStateChangeListener(x => x.name, newName => { wasCalledOnMainThread = MainThread.isMainThread; }, triggerInstantToInit: false); await TaskV2.Run(async() => { store.Dispatch(new ChangeName() { newName = "Caaarl" }); }); Assert.AreEqual("Caaarl", store.GetState().name); Assert.IsFalse(wasCalledOnMainThread); }
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); }