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: