Thursday, March 17, 2011

WPF - Performance Improvement for MVVM Based Applications - Part # 3

This is third part of our discussion about .net features and techniques to improve the performance of MVVM based applications. The other part of this discussion are:

Part 1: http://shujaatsiddiqi.blogspot.com/2011/01/wpf-performance-improvement-for-mvvm.html

Part 2: http://shujaatsiddiqi.blogspot.com/2011/02/wpf-performance-improvement-for-mvvm.html

In this post we will be discussing how we can use Caching to improve the performance of an application. Support of Caching in desktop application is a new feature of .net framework 4.0.

Assembly and Namespace:
Most of the classes used for caching feature are available in System.Runtime.Caching namespace available in System.Runtime.Caching assembly. We need to add an assembly reference of this assembly in order to use these types.


Extensible Caching Implementation:
The cache system available in .net 4.0 has been implemented from ground up to be an extensible concept. A Cache provider must inherit from ObjectCache available in System.Runtime.Caching namespace. The cache provider available with .net is MemoryCache. It represents an in-memory cache. This is similar to ASP.net cache but you don't need to use System.Web.Caching as it is available in the same System.Runtime.Caching namespace in System.Runtime.Caching assembly. The other benefit is that we can create multiple instances of MemoryCache in the same AppDomain.


Simple Caching Usage for temporal locality of reference:
Let's consider an example of a WPF application using this. In this example we would be utilizing the idea of temporal locality of reference. We would be reading the contents of a File. Since I/O is a time consuming operation, we will be caching the contents of the file. We will be using this cached content from other window, where we would be showing it in a TextBlock. The definition of MainWindow is as follows:
<Window x:Class="WpfApp_MVVM_Caching.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp_MVVM_Caching"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Height="81" HorizontalAlignment="Left" Margin="12,17,0,0" 
                   Name="textBlock1" Text="{Binding FileContents}" VerticalAlignment="Top" Width="468" />
        <Button Content="Open Child Window" Height="26" HorizontalAlignment="Left" Margin="12,273,0,0" Name="btnOpenChildWindow" 
                VerticalAlignment="Top" Width="134" Click="btnOpenChildWindow_Click" />
    </Grid>
</Window>
This window has a TextBlock to display the contents of the file. The Text property of this TextBlock is bound to FileContents property of DataContext. The window also has a button. The definition of click event handler [btnOpenChildWindow_Click] from the code behind is as follows:
private void btnOpenChildWindow_Click(object sender, RoutedEventArgs e)
{
    new ChildWindow().Show();
}
This is just opening the ChildWindow as a modeless window using its Show method. MainWindow is using MainWindowViewModel instance as its DataContext. It is constructing its instance as part of its initialization code. The definition of MainWindowViewModel is as follows:
class MainWindowViewModel : INotifyPropertyChanged
{

    public MainWindowViewModel()
    {
        string localfileContents = File.ReadAllText(@"C:\Users\shujaat\Desktop\s.txt");
        FileContents = localfileContents;

        MemoryCache.Default.Set("filecontents", localfileContents, DateTimeOffset.MaxValue);
    }

    #region Properties
    
    private string _fileContents;
    public string FileContents
    {
        get { return _fileContents; }
        set
        {
            _fileContents = value;
            OnPropertyChanged("FileContents");
        }
    }

    #endregion

    #region INotifyPropertyChanged implementation

    public event PropertyChangedEventHandler PropertyChanged = delegate { };
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion INotifyPropertyChanged implementation

}
As expected by the view, It just has a property FileContents. In the constructor, the contents of a file is read and assigned to this property. Since this is based on INotifyPropertyChanged, the changes are propagated to the view using PropertyChanged event. Additionally, we are copying the data read from the file to MemoryCache.Default with Key 'fileconents'.

The definition of ChildWindow is as follows:
<Window x:Class="WpfApp_MVVM_Caching.ChildWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp_MVVM_Caching"
        Title="Child Window" Height="300" Width="300">
    <Window.DataContext>
        <local:ChildWindowViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Height="61" HorizontalAlignment="Left" Margin="36,49,0,0" Name="textBlock1" 
                   VerticalAlignment="Top" Width="206" Text="{Binding CacheEntryValue}" />
    </Grid>
</Window>
It has a TextBlock whose Text property is bound to CacheEntryValue property from the DataContext. The DataContext is an instance of ChildWindowViewModel. Its definition is as follows:
class ChildWindowViewModel : INotifyPropertyChanged
{
    private string _cacheEntryValue;
    public string CacheEntryValue
    {
        get { return _cacheEntryValue; }
        set
        {
            _cacheEntryValue = value;
            OnPropertyChanged("CacheEntryValue");
        }
    }

    public ChildWindowViewModel()
    {
        CacheEntryValue = MemoryCache.Default["filecontents"] as string;
    }

    #region INotifyPropertyChanged implementation

    public event PropertyChangedEventHandler PropertyChanged = delegate { };
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion INotifyPropertyChanged implementation
}
We are just copying the contents of the cache entry defined in the MainWindow to the Text property of the TextBlock. Using the value is achieved using the idea of temporal locality of Caching technology. Let's run the application and open child window.


As you might already have guessed, the contents of the file are: "Muhammad Shujaat Siddiqi".

Changes in Underlying DataSource:
Cache keeps the data available in an application for faster data access when it is needed. The user of this cache might or might not know about the actual data source. The caching feature available in .net framework 4.0 keeps track of this is through the provision of ChangeMonitor. This is basically kind of implementation of Observer design pattern. So when underlying datasource is changed, the ChangeMonitor notifies the ObjectCache implemenation [MemoryCache] about this change. ObjectCache implementation then takes any action about this change. It might even nullify the Cache entry as it is invalid now [Like the case of MemoryCache].


Let us create a new Window to present an example of a WPF application using ChangeMonitor. The Window below is a similar window as the above example. It just has an extra TextBox and a Button.
<Window x:Class="WpfApp_MVVM_Caching.MainWindowChangeMonitorExample"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp_MVVM_Caching"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowChangeMonitorExampleViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Height="81" HorizontalAlignment="Left" Margin="12,17,0,0" 
                   Name="textBlock1" Text="{Binding FileContents}" VerticalAlignment="Top" Width="468" />
        <TextBox Height="138" HorizontalAlignment="Left" Margin="279,125,0,0" Name="textBoxCachedFileContents"
                 VerticalAlignment="Top" Width="212" Text="{Binding Path=MessageToDataSource, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Update Data Source" Height="30" HorizontalAlignment="Left" 
                Margin="314,269,0,0" Name="btnUpdateDataSource" VerticalAlignment="Top" 
                Width="177" Command="{Binding UpdateDataSourceCommand}" />
        <Button Content="Open Child Window" Height="26" HorizontalAlignment="Left" Margin="12,273,0,0" Name="btnOpenChildWindow" 
                VerticalAlignment="Top" Width="134" Click="btnOpenChildWindow_Click" />
    </Grid>
</Window>
The above view is using a new instance of MainWindowChangeMonitorExampleViewModel as its DataContext. It is expecting a string property FileContents to bind to the Text property of the TextBlock. Additionally, it is also expecting the DataContext to have MessageToDataSource property to bind to the Text property of the only TextBox. When a user clicks the Update button the contents of TextBox should be updated to the DataSource (File). Since FileContents are invalid now, it should be updated to the latest contents. For that, the view model should use ChangeMonitor. But first look at the code behind of this view.
public partial class MainWindowChangeMonitorExample : Window
{
    public MainWindowChangeMonitorExample()
    {
        InitializeComponent();
    }

    private void btnOpenChildWindow_Click(object sender, RoutedEventArgs e)
    {
        new ChildWindow().Show();
    }
}
This is similar to the view in the previous examples. When user clicks "Open Child Window", a new instance of ChildWindow is shown in a modeless fashion. Now we have a look at the View Model. Get ready to see Change Monitors in action...
class MainWindowChangeMonitorExampleViewModel : INotifyPropertyChanged
{
    #region Fields

    List<string> filePaths = new List<string>();

    #endregion Fields

    #region Constructors

    public MainWindowChangeMonitorExampleViewModel()
    {
        filePaths.Add(@"C:\Users\shujaat\Desktop\s.txt");
        
        string localfileContents = File.ReadAllText(@"C:\Users\shujaat\Desktop\s.txt");
        FileContents = localfileContents;

        var changeMonitor = new HostFileChangeMonitor(filePaths);
        var policy = new CacheItemPolicy();

        MemoryCache.Default.Set("filecontents", localfileContents, policy);
                                 
        policy.ChangeMonitors.Add(changeMonitor);            
        changeMonitor.NotifyOnChanged(OnDataSourceUpdated);

    }

    #endregion Constructors

    #region Change Monitor Callback

    private void OnDataSourceUpdated(Object State)
    {
        //Get file contents
        FileInfo file = new FileInfo(@"C:\Users\shujaat\Desktop\s.txt");
        using (FileStream stream = file.OpenRead())
        {
            using (StreamReader reader = new StreamReader(stream))
            {
                string updatedFileContents = reader.ReadToEnd();
                reader.Close();
                FileContents = updatedFileContents;
            }
        }

        //Update Cache Entry
        MemoryCache.Default["filecontents"] = FileContents;

        //Update change monitor for further changes in file contents
        var policy = new CacheItemPolicy();
        var changeMonitor = new HostFileChangeMonitor(filePaths);
        policy.ChangeMonitors.Add(changeMonitor);
        changeMonitor.NotifyOnChanged(OnDataSourceUpdated);
    }

    #endregion Change Monitor Callback

    #region Properties

    private string _fileContents;
    public string FileContents
    {
        get { return _fileContents; }
        set
        {
            _fileContents = value;
            OnPropertyChanged("FileContents");
        }
    }

    private string _messageToDataSource;
    public string MessageToDataSource
    {
        get { return _messageToDataSource; }
        set
        {
            _messageToDataSource = value;
            OnPropertyChanged("MessageToDataSource");
        }
    }

    #endregion

    #region INotifyPropertyChanged implementation

    public event PropertyChangedEventHandler PropertyChanged = delegate { };
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion INotifyPropertyChanged implementation

    #region Commands

    ICommand _updateDataSourceCommand;
    public ICommand UpdateDataSourceCommand
    {
        get
        {
            if (_updateDataSourceCommand == null)
            {
                _updateDataSourceCommand = new RelayCommand(
                    param => this.UpdateDataSource(),
                    param => this.CanUpdateDataSource
                    );
            }
            return _updateDataSourceCommand;
        }
    }

    public void UpdateDataSource()
    {
        FileInfo file = new FileInfo(@"C:\Users\shujaat\Desktop\s.txt");
        using (FileStream stream = file.OpenWrite())
        {
            using (StreamWriter writer = new StreamWriter(stream) { AutoFlush = true })
            {
                writer.Write(MessageToDataSource);
                writer.Close();
            }
        }
    }

    bool CanUpdateDataSource
    {
        get { return true; }
    }

    #endregion Commands
}
In the constructor, like in the previous example, it is reading the contents of a text file and adding it to the cache. Additionally, it is creating a HostFileChangeMonitor and adds it the CacheItemPolicy's list of ChangeMonitors. This policy is being used for keeping a watch on the contents of the files specified by the ChangeMonitors. If there is an update, it executes the callback as specified by the ChangeMonitor. HostFileChangeMonitor is a descendent of ChangeMonitor through FileChangeMonitor. In non ASP.net applications, it uses internally FileSystemWatcher to monitor files. It must be remembered that it does not support relative paths.

As expected by the view, it has two string properties FileContents and MessageToDataSource. It also has a RelayCommand object UpdateDataSourceCommand. This command is executed when user clicks Update Data Source button. In the execute method [UpdateDataSource] of this command, we are just updating the contents of the file using StreamWriter.

The soul of this example is OnDataSourceUpdated method specified as the callback for change notification in the monitored data sources by HostFileChangeMonitor. When the contents of the file are updated, this method gets called. In this method we are updating the Cache Entry and the string property bound to the text block in the view showing the contents of the file.

Now specify the new view as startup view of the project in App.xaml and run the application. The view appears as follows:


Now let us update the contents of the TextBox and click "Update Data Source" button. As the contents of the file are updated, the callback is called by the ChangeMonitor which updates the cache entry and the FileContent property with updated contents of the file. Since this is bound to a TextBlock.TextProperty in the view, the view shows this update.


Now we open the child window using "Open Child Window" button. The child window uses the same cache entry. The child window appears as follows:


Now update the contents of file again by updating the contents of the TextBox and hitting "Update Data Source" button. This should be done when the ChildWindow is still shown. We are updating the contents with one of Jalal Uddin Rumi's great quote.


Cache Entry Change Monitor:

As you can see that clicking Update Source button after updating the contents of the TextBox updates the TextBlock on the main window but it does not appear to update the contents of second window. This is because the ChildWindow does not know that the cache entry value it used for its TextBlock has been updated. If ChildWindow has some means to get notification for this cache entry update then it could update its logic. For this purpose we can use another ChangeMonitor, called CacheEntryChangeMonitor.

Let's update the constructor of ChildWindowViewModel as follows:
public ChildWindowViewModel()
{
    CacheEntryValue = MemoryCache.Default["filecontents"] as string;
    MemoryCache.Default.CreateCacheEntryChangeMonitor(
        new string[] { "filecontents" }.AsEnumerable<string>()).NotifyOnChanged((a) =>
        {
            CacheEntryValue = MemoryCache.Default["filecontents"] as string;
        }
    );
}
Here we have just added an instane CacheEntryChangeMonitor to the code. Again we are using the same cache entry but the only difference we have added a NotifyOnChanged callback for this. As the entry gets updated this callback is called and it updates the property bound to the TextBlock.Text property in the view refreshing this.


Download Code:

No comments: