Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Redux and the State of My XAML Application (part 2)

In part 1 I talked a bit about the challenge that XAML based applications face when trying to use a pattern such as Redux. In this post I’m going to jump in and use Redux.NET to demonstrate the issue, and the show how we can make a small adaption to the XAML to fix this issue.

We’ll start with the basic – our application state is a Person entity, with a Name property and a Family property. The Family property is an ObservableCollection of Person:

public class Person : NotifyBase
{
     public string Name { get; set; }

    public ObservableCollection<Person> Family { get; set; } = new ObservableCollection<Person>();
}

In this case NotifyBase comes from the BuildIt.General library and implements INotifyPropertyChanged. It also implements IRaisePropertyChanged which exposes a RaisePropertyChanged method which can be called in order to raise a PropertyChanged event on the object – we’ll come to why this is important later.

Implementing the Redux pattern starts with the Store, and in this case I’m just going to expose this as a static property off the App class. In reality you’d probably register this with your IoC container and have it injected into your ViewModel but to keep things simple I’m just creating it as a static property.

sealed partial class App : Application
{
     public static IStore<Person> PersonStore { get; private set; } = new Store<Person>(reducer: PersonReducer.Execute, initialState: new Person { Name = "Fred" });

The Store of course requires a Reducer, which in this case will be the PersonReducer class

public static class PersonReducer
{
     private static Random rnd = new Random();
     public static Person Execute(Person state, IAction action)
     {
         if (action is AddAction addAction)
         {
             var newPerson = new Person { Name = addAction.NameOfNewFamilyMember };

            return new Person
             {
                 Name = state.Name,
                 Family = state.Family.DeepClone().AddItem(newPerson)
             };
         }

        if (action is RemoveAction)
         {
             var idxToRemove = rnd.Next(0, 1000) % state.Family.Count;
             return new Person
             {
                 Name = state.Name,
                 Family = state.Family.DeepClone().RemoveItemAt(idxToRemove)
             };
         }

        return state;
     }
}

As you can see from the code the PersonReducer implements two actions: AddAction and RemoveAction. We’ll create these as classes

public class AddAction : IAction
{
     public string NameOfNewFamilyMember { get; set; }
}

public class RemoveAction : IAction { }

The other thing to note about the PersonReducer is that both actions return entirely new Person entities. It also makes use of a couple of helper methods:

public static class ReduxHelpers
{

    public static ObservableCollection<T> DeepClone<T>(this ObservableCollection<T> source) where T : new()
     {
         var collection = new ObservableCollection<T>();
         var helper = TypeHelper.RetrieveHelperForType(typeof(T));
         foreach (var item in source)
         {
             var newItem = new T();
             helper.DeepUpdater(newItem, item);
             collection.Add(newItem);
         }
         return collection;
     }

    public static ObservableCollection<T> AddItem<T>(this ObservableCollection<T> source, T itemToAdd)
     {
         source.Add(itemToAdd);
         return source;
     }

    public static ObservableCollection<T> RemoveItemAt<T>(this ObservableCollection<T> source, int index)
     {
         if (index < 0 || index >= source.Count) return source;
         source.RemoveAt(index);
         return source;
     }
}

Note: These extension methods will be added to BuildIt.General in the coming days and they rely on other types/methods (such as the TypeHelper class) that are already part of the BuildIt.General library.

With the Store and Reducer defined, we can define our MainViewModel

public class MainViewModel : NotifyBase, IHasImmutableData<Person>
{
     private Person data;
     public Person Data
     {
         get => data;
         set => SetProperty(ref data, value);
     }

    public MainViewModel()
     {
         App.PersonStore.Subscribe(newData => Data = newData);
     }
}

As this code shows, when the state in the Store changes, we’re just updating the Data property on the MainViewModel, this will in turn raise the PropertyChanged event causing the UI to be re-bound. Let’s take a look at the XAML for the MainPage.xaml:

<Page
     x:Class="reduxsample.MainPage"
     xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:converters="using:BuildIt.UI.Converters"
     mc:Ignorable="d"
     Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
     <Page.Resources>
         <converters:ImmutableDataConverter x:Key="ImmutableDataConverter"/>
     </Page.Resources>

     <Grid DataContext="{Binding Converter={StaticResource ImmutableDataConverter}}">
         <Grid DataContext="{Binding Data}">
             <Grid.RowDefinitions>
                 <RowDefinition Height="Auto"/>
                 <RowDefinition />
             </Grid.RowDefinitions>
             <StackPanel Grid.Row="0">
                 <TextBlock Text="Name"/>
                 <TextBlock Text="{Binding Name}"/>
                 <TextBlock Text="Family member to add"/>
                 <TextBox x:Name="NewFamilyMemberName"/>
                 <Button Content="Add" Click="AddFamilyClick"/>
                 <Button Content="Remove" Click="RemoveFamilyClick"/>
             </StackPanel>
             <ListView Grid.Row="1" ItemsSource="{Binding Family}">
                 <ListView.ItemTemplate>
                     <DataTemplate>
                         <Border
                             BorderBrush="Azure"
                             BorderThickness="0,0,0,1">
                             <TextBlock Text="{Binding Name}"/>
                         </Border>
                     </DataTemplate>
                 </ListView.ItemTemplate>
             </ListView>
         </Grid>
     </Grid>
</Page>

This is all stock XAML with the exception of the Bold text (which we’ll come to in a minute). First, let’s add the methods for adding and removing family members

public sealed partial class MainPage : Page
{
     public MainPage()
     {
         this.InitializeComponent();

        DataContext = new MainViewModel();
     }
     private void AddFamilyClick(object sender, RoutedEventArgs e)
     {
         App.PersonStore.Dispatch(new AddAction { NameOfNewFamilyMember = NewFamilyMemberName.Text });
     }
     private void RemoveFamilyClick(object sender, RoutedEventArgs e)
     {
         App.PersonStore.Dispatch(new RemoveAction());
     }
}

Ok, now for a minute, let’s go back to the XAML and for a minute imagine that the bold text isn’t there. Now when we run the app and click the Add or Remove button, the Data property on the MainViewModel will get repopulated – the implication on the UI is that the ListView which binds to the Family property will refresh entirely. This will cause a flicker, and will drop any selected state and/or scroll position on the list – generally a really bad user experience.

With the bold text left in the XAML, when we run the app and click the Add or Remove button, only the properties that have changed are refreshed – there is no flicker on the ListView and any selection/scroll position is maintained….. so what’s going on…. can you work out why IRaisePropertyChanged is important…. more to come in my next post.

Comments are closed