Introduction
In the Part 1 of the article series, we have started the work on changing to MVVM and MEF composable patterns for a Silverlight application previously with basic navigation and code-behind patterns. By the end of the Part 1, the application is capable of loading a xap, exporting a class module to the composition container, and rendering the same screen to the browser as that before the changes. We will implement the composable MVVM modules for the MainPage user control, the Product List parent screen, and the child window in this part based on the architecture design shown from the beginning of the Part 1.
Contents and Links
Setting up the MVVMLight Library
Using the NuGet is nowadays a recommended approach to set up libraries for an application developed with the Visual Studio. But I would not like the way that, if downloaded from the NuGet using the Visual Studio, the GalaSoft MVVMLight adds assemblies for all old versions of .NET Framework with total size of 2.5 MB. Instead we just need two dll files, GalaSoft.MvvmLight.SL5.dll and System.Windows.Interactivity.dll, with total size of just 56 KB. Let’s manually load these two files this time and set the references from the projects that require them.
-
Create a physical folder and name it as _Assemblies in the solution root folder using the Windows Explorer. This folder can be used as a location of all shared assembly sources.
-
Copy and paste two files GalaSoft.MvvmLight.SL5.dll and System.Windows.Interactivity.dll to the _Assemblies folder. You can find these file in the _Assemblies folder from the downloaded source code package for this part of the article series.
-
It’s not necessary for the shared assembly folder and files to be displayed on the Solution Explorer. But if you want to, you can create a virtual solution folder with the same name _Assemblies under the solution root. Copy the files from the physical _Assemblies folder and then paste the files to the virtual folder on the Solution Explorer. The files shown in the Solution Explorer are the virtual copies.
-
Create the references of the two dll assemblies from the ProductApp.Main, ProductApp.Views, and ProductApp.Common projects using the Browse tab on the Add Reference screen.
The MVVMLight library provides the commanding, messaging, and clean-up features that reduce our extra coding efforts. We will directly call the functions in the library but for sending messages and displaying dialog text, we add two new class files, MessageToken.cs and StaticText.cs, into the Constants folder of the ProductApp.Common project. You can also find these files in the downloaded source code package.
Using a ViewModel for the MainPage
As designed, the MainPage.xaml with its code-behind is the main content holder (or switch board) so that it doesn’t have the Model for data processing. We need to move the processes from the code-behind to the MainPageViewModel class except those related to the UI and loading View modules.
-
Add the System.ComponentModel.Composition (.NET) reference into the ProductApp.Main project.
-
Add a new folder with the name of ViewModels into the ProductApp.Main project and a new class file, MainPageViewModel.cs, into the folder.
-
Replace the auto generated code in the MainPageViewModel.cs with the code shown below. The MainPageViewModel
class inherits the ViewModelBase
class from the MVVMLight that has the RaisePropertyChanged
and Cleanup
functions. Now the MainPageViewModel
class takes the responsibility to load the xap and then sends the message back to the View code-behind for importing the module from the xap.
using System;
using System.Linq;
using System.Windows.Controls;
using System.ComponentModel;
using System.ComponentModel.Composition;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
using ProductApp.Common;
namespace ProductApp.Main.ViewModels
{
[Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.MainPageViewModel)]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MainPageViewModel : ViewModelBase, IModule
{
private ModuleCatalogService _catalogService = ModuleCatalogService.Instance;
private string _currentViewText = string.Empty;
public MainPageViewModel()
{
}
private RelayCommand<string> _loadModuleCommand;
public RelayCommand<string> LoadModuleCommand
{
get
{
if (_loadModuleCommand == null)
{
_loadModuleCommand = new RelayCommand<string>(
OnLoadModuleCommand,
moduleId => moduleId != null);
}
return _loadModuleCommand;
}
}
private void OnLoadModuleCommand(String moduleId)
{
string xapUri;
try
{
if (_currentViewText != moduleId)
{
switch (moduleId)
{
case ModuleID.ProductListView:
xapUri = "/ClientBin/ProductApp.Views.xap";
_catalogService.AddXap(xapUri, arg => ProductApp_OnXapDownloadCompleted(arg));
break;
default:
throw new NotImplementedException();
}
}
}
catch (Exception ex)
{
Messenger.Default.Send(ex, MessageToken.RaiseErrorMessage);
}
}
private void ProductApp_OnXapDownloadCompleted(AsyncCompletedEventArgs e)
{
Messenger.Default.Send(ModuleID.ProductListView, MessageToken.LoadScreenMessage);
_currentViewText = ModuleID.ProductListView;
}
}
}
-
Change the attributes of the HyperlinkButton
in the code of MainPage.xaml to have the Command
and CommandParameter
sent to the MainPageViewModel. The path name of the Command
should be the same as the name of the property with the RelayCommand
type. The value of the CommandParameter
should also be the same as the ModuleID
.
<HyperlinkButton x:Name="linkButton_ProductList"
Style="{StaticResource LinkStyle}"
Content="Product List"
Command="{Binding Path=LoadModuleCommand}"
CommandParameter="ProductListView" />
-
Replace the existing code in the MainPage.xaml.cs with the code shown below. The code-behind registers the message handlers, loads the exported View modules, dynamically changes the UI properties, and displays the error or information messages, if any, on dialog boxes.
using System;
using System.Windows;
using System.Windows.Controls;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Messaging;
using ProductApp.Common;
namespace ProductApp.Main.Views
{
public partial class MainPage : UserControl
{
private ModuleCatalogService _catalogService = ModuleCatalogService.Instance;
public MainPage()
{
InitializeComponent();
Messenger.Default.Register(this, MessageToken.LoadScreenMessage,
new Action<string>(OnLoadScreenMessage));
Messenger.Default.Register(this, MessageToken.RaiseErrorMessage,
new Action<Exception>(OnRaiseErrorMessage));
Messenger.Default.Register(this, MessageToken.UseDialogMessage,
new Action<DialogMessage>(OnUseDialogMessage));
if (!ViewModelBase.IsInDesignModeStatic)
{
DataContext = _catalogService.GetModule(ModuleID.MainPageViewModel);
}
}
private void OnLoadScreenMessage(string moduleId) {
object newScreen;
try
{
switch (moduleId)
{
case ModuleID.ProductListView:
newScreen = _catalogService.GetModule(ModuleID.ProductListView);
break;
default:
throw new NotImplementedException();
}
MainContent.Content = newScreen;
SetLinkButtonState(moduleId);
}
catch (Exception ex)
{
OnRaiseErrorMessage(ex);
}
}
private void SetLinkButtonState(string buttonArg)
{
foreach (UIElement child in LinksStackPanel.Children)
{
HyperlinkButton hb = child as HyperlinkButton;
if (hb != null && hb.Command != null)
{
if (hb.CommandParameter.ToString().Equals(buttonArg))
{
VisualStateManager.GoToState(hb, "ActiveLink", true);
}
else
{
VisualStateManager.GoToState(hb, "InactiveLink", true);
}
}
}
}
private void OnRaiseErrorMessage(Exception ex)
{
ChildWindow errorWin = new ErrorWindow(ex.Message, ex.StackTrace);
errorWin.Show();
}
private void OnUseDialogMessage(DialogMessage dialogMessage)
{
if (dialogMessage != null)
{
MessageBoxResult result = MessageBox.Show(dialogMessage.Content,
dialogMessage.Caption, dialogMessage.Button);
dialogMessage.ProcessCallback(result);
}
}
}
}
-
The application should run fine now with the new command workflow using the MainPageViewModel
class.
Updating the ProductList with Composable MVVM
The tasks of moving processes to the ProductListViewModel from the ProductList code-behind are pretty much the same as for the MainPage
user control. The major differences are that some methods and properties in the ProductListViewModel are associated with data operations in its Model class and data bindings in its View. We will focus more on these differences in this section.
-
Add a virtual folder ProductApp.Client into the solution and drag/drop the existing ProductApp.Views to the virtual folder.
-
Add two new Silverlight Class Library projects with the names of ProductApp.ViewModels and ProductApp.Models under the ProductApp.Client virtual folder. The fast and easy way to do these is to create a custom template of ProductApp.Common, use it for creating the new class library projects, and then delete the unwanted carry-over items in the new projects as we did in the previousl part of the article series. All needed references, except the ProductApp.Common, are already set for the new projects.
-
Add the reference of the ProductApp.Common project into the two new projects.
-
Add references of the two new projects into the ProductApp.Views project. The two new projects for the ViewModel and Model are not the separately loaded assemblies and need a link to the parent project for exporting their modules.
-
Add some folders and files required for the data operations mainly performed in the ProductApp.Models project as shown below.
For accessing ViewModel memebers from a View, we can set the buit-in DataContext
of the View to hold the instance of exported ViewModel. For accessing the Model memebers from a ViewModel, however, we need to create our own interface type. This is what the IProductListModel.cs comes into the play. It follows the loC (inversion of control) pattern standards although it doesn’t provide a fully decoupled scenario.
using System;
using System.ComponentModel;
using GalaSoft.MvvmLight;
using ProductRiaLib.Web.Models;
using ProductRiaLib.Web.Services;
namespace ProductApp.Common
{
public interface IProductListModel : INotifyPropertyChanged, ICleanup
{
void GetCategoryLookup();
event EventHandler<QueryResultsArgs<Category>> GetCategoryLookupComplete;
void GetCategorizedProducts(int categoryId);
event EventHandler<QueryResultsArgs<Product>> GetCategorizedProductsComplete;
void SaveChanges(string operationType);
event EventHandler<SubmitOperationArgs> SaveChangesComplete;
void AddNewProduct(Product addedProduct);
void DeleteProduct(Product deletingProduct);
string CurrentOperation { get; set; }
Boolean HasChanges { get; }
Boolean IsBusy { get; }
}
}
The DataAsyncHandlers.cs contains two wrapper functions to call the Load
and SubmitChanges
methods in the ProductDomainContext
of the RIA Domain Services. We need to call the custom wrapper functions with appropriate custom arguments to wait for returning the datasets after loading a query or getting a status after submitting the data during asynchronous operations in the MVVM and MEF composable patterns. In the applications with basic patterns, the asynchronous issues are automatically handled when the domain context instance is created in the SilverLight User Control code-behind or using the DomainDataSource
control. For example, in our old ProductList.xaml.cs code-behind, we just call the Load
function and get the data form the ctx
like this.
ctx.Load(ctx.GetCategoriesQuery());
When updated to the new patterns, we need to call the Load function by passing four parameters and obtain the dataset from the callback argument e
.
context.Load(ctx.GetCategoriesQuery(), LoadBehavior.RefreshCurrent,
r => { queryResultEvent(s, e); }, null);
The detailed data operations are not our focus of this article series so that we do not show here the code details of DataAsyncHandlers.cs, OperationTypes.cs, and files in the EventArguments folder. You can directly copy and use the files from the downloaded source code pacakge.
-
Add a class file, ProductListModel.cs, to the ProductApp.Models project. The class is exported as a shared module and defines three event handlers for the data operations.
[Export(ModuleID.ProductListModel, typeof(IProductListModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class ProductListModel : IProductListModel
{
private ProductDomainContext _ctx;
public event EventHandler<QueryResultsArgs<Category>> GetCategoryLookupComplete;
public event EventHandler<QueryResultsArgs<Product>> GetCategorizedProductsComplete;
public event EventHandler<SubmitOperationArgs> SaveChangesComplete;
}
The remaining code inside the class is simple and mostly the implementation of the members defined in the IProductListModel
interface. You can copy the code lines or the file from the downloaded source package and then exam the details.
-
Add a class file, ProductListViewModel.cs, to the ProductApp.ViewModels project. The class content implementation is pretty much the same as the MainPageViewModel.cs we previous did except more members associated with CRUD data operation commands and processes. The whole code pieces are not displayed here. You can copy the code from the ProductListViewModel.cs file in the downloaded source package and then exam the details there. Below are additional notes for some particular members and code lines in this class.
The class imports the ProductListModel module by directly calling a method of the composition container and exposes it as the Lazy
object with IProductListModel
type, the way that is a little different from that exporting the ViewModel to the container. The class also receives the events raised from the Model to continue some actions after the asynchronous data operations. This code snippet illustrates the points.
public ProductListViewModel() {
_productListModel = ModuleCatalogService.Container.GetExport<IProductListModel>(ModuleID.ProductListModel).Value;
_productListModel.GetCategoryLookupComplete += ProductListModel_GetCategryComplete;
}
private void ProductListModel_GetCategryComplete(object sender, QueryResultsArgs<Category> e)
{
if (!e.HasError)
{
CategoryItems = e.Results;
if (SelectedCategory == null)
{
CategoryItems.Insert(0, _comboDefault);
SelectedCategory = CategoryItems[0];
}
}
else
{
Messenger.Default.Send(e.Error, MessageToken.RaiseErrorMessage);
}
}
The code in the ProductListViewModel
class also receives the two special commands sent from the View for non-button elements and then performs desired actions. The command trigger issue will be discussed on the List 9.
public RelayCommand CategorySelectionChanged
{
}
private void OnCategorySelectionChanged()
{
Category item = SelectedCategory;
if (item != null && item.CategoryID > 0)
{
_productListModel.GetCategorizedProducts((int)item.CategoryID);
}
if (SelectedCategory != _comboDefault)
{
CategoryItems.Remove(_comboDefault);
}
AddProductCommand.RaiseCanExecuteChanged();
}
public RelayCommand DataGridSelectionChanged
{
}
private void OnDataGridSelectionChanged()
{
SaveChangesCommand.RaiseCanExecuteChanged();
DeleteProductCommand.RaiseCanExecuteChanged();
}
-
Now update the code in the ProductList.xaml.cs code-behind. Only very simple code lines are placed in the class.
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel.Composition;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Messaging;
using ProductApp.Common;
namespace ProductApp.Views
{
[Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.ProductListView)]
public partial class ProductList : UserControl, IModule
{
private ModuleCatalogService _catalogService = ModuleCatalogService.Instance;
public ProductList()
{
InitializeComponent();
if (!ViewModelBase.IsInDesignModeStatic)
{
DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.ProductListViewModel);
}
}
}
}
-
Update the existing ProductList.xaml file by copying/pasting the code lines or the whole file from the downloaded source package. Note that the System.Windows.Interactivity
reference is added into the xmlns
declaring section.
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
The code lines for the ComboBox
, DataGrid
, and Buttons
are changed. The Command
binding path points to the property of the RelayCommand
type. There is an additional EventTrgger
node defined using the System.Windows.Interactivity
assembly with the built-in event SelectionChanged
attached to it for the ComboBox
. The xaml code actually sets the Command
property of the System.Windows.Interactivity.InvokeCommandAction
class to the property of the RelayCommond
type in the ProductListViewModel
class. The same is implemented for the DataGrid
.
<ComboBox Height="23" Margin="6"
Name="categoryCombo" Width="150"
ItemsSource="{Binding Path=CategoryItems}"
DisplayMemberPath="CategoryName"
SelectedValuePath="CategoryID"
SelectedItem="{Binding Path=SelectedCategory, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding CategorySelectionChanged}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
Making Changes in the Child Window
The child window AddProductWindow.xaml shares the ProductListModel module for data operation with the parent. The child window is also opened from the parent View, not from the MainPage View. All communications between the parent screen and child window occur between the parent ViewModel and child ViewModel through the fully decoupled event massaging approaches.
-
Add the new class file, AddProductWindowViewModel.cs, into the ProductApp.ViewModels project. Update the code or replace the file with that from downloaded source package. All export settings and command properties are similar to those we have done in other modules. The class registers a message handler to receive the SelectedCategory
object required for the data binding.
public AddProductWindowViewModel() {
Messenger.Default.Register(this, MessageToken.DataToAddProductVmMessage,
new Action<Category>(OnDataToAddProductVmMessage));
}
private void OnDataToAddProductVmMessage(Category selectedCategory)
{
SelectedCategory = selectedCategory;
AddedProduct = new Product();
}
When receiving the command for adding a product to database, the class sends the first message with the AddedProduct
object to the parent ViewModel for data updates. It then sends another message with the status information to the parent View for display.
private void OnOKButtonCommand()
{
if (AddedProduct != null)
{
AddedProduct.CategoryID = SelectedCategory.CategoryID;
Messenger.Default.Send(AddedProduct, MessageToken.DataToProductListVmMessage);
Messenger.Default.Send("OK", MessageToken.AddProductWindowMessage);
}
}
On the parent side, the ProductListViewModel module registers the message handler to receive the AddedProduct
object and uses the event routine to call for data operations and refreshing the display.
public ProductListViewModel() {
Messenger.Default.Register(this, MessageToken.DataToProductListVmMessage,
new Action<Product>(OnDataToProductListVmMessage));
}
private void OnDataToProductListVmMessage(Product addedProduct)
{
if (!_productListModel.IsBusy && addedProduct != null)
{
_productListModel.AddNewProduct(addedProduct);
ProductItems.Add(addedProduct);
}
}
-
Update the AddProductWindow.xaml.cs file by copying the code or file from the downloaded source package. In addition to setting its DataContext
to an instance of the exported AddProductWindowViewModel, the class also receives the message from the ViewModel for taking responses to the OK or Cancel button clicking actions.
public AddProductWindow() {
InitializeComponent();
Messenger.Default.Register(this, MessageToken.AddProductWindowMessage,
new Action<string>(OnAddProductWindowMessage));
if (!ViewModelBase.IsInDesignModeStatic)
{
DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.AddProductWindowViewModel);
}
}
private void OnAddProductWindowMessage(string buttonName)
{
switch (buttonName)
{
case "OK":
DialogResult = true;
break;
case "Cancel":
DialogResult = false;
break;
default:
break;
}
}
-
Changes in the AddProductWindow.xaml file are the command binding settings and the data bindings to the text boxes. The code lines are similar to those in the parent xaml file. The code is listed here. You can update the AddProductWindow.xaml file by copying the code or the file from the downloaded source package.
-
Add code lines to the parent View ProductList.xaml.cs code-behind for loading and opening the child window.
private ChildWindow _addProdScreen;
public ProductList() {
Messenger.Default.Register(this, MessageToken.LoadAddProductViewMessage,
new ActionA<string>(OnLoadAddProductViewMessage));
}
private void OnLoadAddProductViewMessage(string message)
{
_addProdScreen = _catalogService.GetModuleLazy(ModuleID.AddProductWindowView) as ChildWindow;
_addProdScreen.Show();
}
-
Run the application. The Product List screen and the Add Product Window now work with the MVVM and MEF composable patterns although the screens and contents are shown as the same as those with basic patterns before.
Summary
In this part of the article series, we have set up the MVVMLight library used by the application. We have then upgraded the MainPage control and the Product List screen with its child window from basic patterns to the MVVM and MEF composable patterns. In the Part 3, we will add another screen into the ProductApp.Main project, create another set of projects in the solution for a new xap assembly, implement the module clean-up processes, and add a state persistence feature into the application.