GuillaumeSalles / redux.NET

Redux.NET is a predictable state container for .NET apps. Inspired by https://github.com/reactjs/redux.
712 stars 86 forks source link

Question about Dispatching Actions. #67

Open p-sheth opened 7 years ago

p-sheth commented 7 years ago

Hi,

I am working on Xamarin application and have been using MVVM so far. I am now trying to incorporate Redux to manage the state.

My question is, which a good place to call Store.Dispatch()? Should I keep it in code behind of Xaml or viewmodel?

It works either ways, but I want to understand which is a better practice? Should I continue using ViewModels after adopting Redux or just wire the state directly to View?

Thanks

cmeeren commented 7 years ago

Both works. I've seen viewmodels used; personally I just use the codebehind, because I don't need much logic in there for my app (and also I want to make it easy to port from Xamarin.Forms to native Xamarin.Android/iOS for experimentation, which AFAIK do not support data binding out of the box).

jonathandbailey commented 6 years ago

Unless it’s a throwaway prototype, you shouldn’t put any access to the Store into the code-behind.

MVVM allows you to write tests that interact with your ViewModel and simulate view interactions without the view by giving you a clear separation of UI from non-UI logic. Communicating with your Store in the code behind is therefore technically an MVVM violation because, without the view, you can’t test this code.

MVVM and Redux don’t fit well together because MVVM and bindings break the one-way data flow of Redux/React Architecture. MVVM and bindings also make direct changes to the target property they are bound to on the model, but this is impossible when you have an immutable store with immutable models.

A typical pattern you see with MVVM is some data retrieval, via a service or repository data source, in the ViewModel. SOLID and SRP purists would have a problem with this as a ViewModel should only have one responsibility, and that’s to adapt your model to the view. Once your application grows, it makes more sense to keep the logic in the Model and wrap it with a ViewModel when interactions like Commands are required.

A possible solution to your question:

  1. Create a Model in your application to model some business logic. As an example let’s put a property on it called ‘Name’ which emits property changed events from the setter.
  2. Bind a control in your XAML view to this property and set your model as data context.
  3. Inject your store into your model, read the current state and set the ‘Name’ property.
  4. The binding update from RaisePropertyChanged will then show the ‘Name’ value in the UI.
  5. Listen to your Store for state change events and Update the ‘Name’ property on your model when it changes.
  6. When the User changes the Name value via the UI and the setter is called on the ‘Name’ property create an UpdateAction and Dispatch it to your store.

This will keep your Store and MVVM cleanly separated, which in my opinion is a good thing, as they are solving two different problems.

cmeeren commented 6 years ago

I've moved to F# (not really relevant for this comment) and am using a solution I find very clean and easily testable, which allows for unidirectional data flow of most state while using MVVM/INotifyPropertyChanged for e.g. input field state (which doesn't work well with redux - dispatching every new value of text fields to the store has several problems, e.g. poor performance and chance of triggering infinite loops since you're in a sense setting the text field value from its TextChanged event handler, with no optimal solutions).

What I do is this (using a sign-in page as example, and rough pseudocode as I'm writing this from the top of my head and haven't used C# in a while):

  1. The store state (and everything in my main business logic) is immutable and works according to normal Redux patterns.

  2. My view model, SignInVm, has a corresponding class called SignInVmData that is simply the relevant parts of the Redux state. The view model itself, SignInVm, has a mutable property of type SignInVmData. (The reason I use a separate class called SignInVmData instead of just using the root redux state is that only having to deal with the relevant subset of the root state makes the VM much easier to test.) The view model also has any other relevant mutable fields for state that is difficult to keep in the redux state, e.g. text fields (username and password) and current time (when needed to e.g. calculate the time since some timestamp in the redux state).

class SignInVmData {
  SignInVmData(RootState state) {
    // The constructor is uninteresting to test since it just maps the redux state
    IsSigningIn = state.SignIn.IsSigningIn;
    LastSignIn = state.SignIn.LastSignIn;
  }

  // for easier testing (F# solves this more robustly with less boilerplate)
  SignInVmData() { } 

  public bool IsSigningIn {get; set;}
  public DateTimeOffset LastSignIn {get; set;}
}

class SignInVm {

  SignInVm(SignInVmData data) {
    _data = data;
  }

  public SignInVmData _data {get; set;}
  public DateTimeOffset _now {get; set;} = DateTimeOffset.Now
  public string Username {get; set;}
  public string Password {get; set;}

  public bool InputFieldsEnabled => 
    !_data.IsSigningIn;

  public bool CanSignIn => 
    !string.IsNullOrEmpty(Username)
    && !string.IsNullOrEmpty(Password)
    && !_data.IsSigningIn;

  // Fairly pointless thing on a sign-in screen, it's here just to demo the time stuff
  public string SecondsSinceLastSignIn =>
    (_now - _data.LastSignIn).TotalSeconds.ToString();

  public bool SignInAction =>
    CanSignIn ? SignInAction(Username, Password) : null;
}
  1. As you can see above, all other properties (InputFieldsEnabled, CanSignIn, etc.) are derived from the mutable properties. This makes things trivially testable. It does of course require some way to handle INotifyPropertyChanged dependencies among the properties - I use Nito.Mvvm.CalculatedProperties which is great, you might also want to check out PropertyChanged.Fody for automatic weaving with no boilerplate but it's much more limited (e.g. no cross-class dependencies if you ever need that, and no lambdas in F# at least because those are compiled as separate classes).

  2. Note that I also have (calculated) properties for all actions that the view can dispatch to the store. These are surfaced as simple action objects, and bound to from XAML ICommand properties using a custom IValueConverter (that gets the store injected in static property at app startup) that converts an action object to an ICommand that dispatches this action to the store. The commands are "immutable" in that CanExecute never changes. It can always execute unless the action is null (None in F#), and as you have seen, it's the VM that controls this. This makes the action dispatching simple to test and fairly foolproof in practical use, since a null action of course can not possibly lead to an erroneous action being dispatched to the store (at worst you get a NullReferenceException in your converter which you discover quickly, fix once and never hear from again for any properties on any VM).

  3. All views take in a store instance in the constructor, instantiates the VM (parameterless constructor), and wires up changes to the store state (mapped to SignInVmData) to the mutable property on the VM. If the VM has a mutable property used for the current time, that is wired up too (using Observable.Interval or your favorite timer function). I use weak references for all of this so I don't have to think about unsubscribing to avoid memory leaks.

class SignInPage : MyViewBaseClass {
  SignInPage(Store store) {
    var vm = new SignInVm(new SignInVmData(store.State));
    this.BindingContext = vm;
    store.StateChanged += state => vm._data = SignInVmData(state)
    SomeOnceASecondTimer.Elapsed += () => vm._now = DateTimeOffset.Now
  }
}

All in all, the advantages are: