Click here to Skip to main content
15,867,330 members
Articles / Programming Languages / C#

Migrate from Basic to MVVM and MEF Composable Patterns for a Silverlight Application - Part 2

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
3 May 2012CPOL10 min read 21.9K   439   8  
The article series shows how to upgrade a Silverlight application having basic patterns to the MVVM and MEF composable patterns with easy approaches and detailed coding explanations.

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.

  1. 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.

  2. 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.

  3. 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.

    21.png

  4. 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.

  1. Add the System.ComponentModel.Composition (.NET) reference into the ProductApp.Main project.

  2. Add a new folder with the name of ViewModels into the ProductApp.Main project and a new class file, MainPageViewModel.cs, into the folder.

    22.png

  3. 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.

    C#
    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()
            {            
            }
    
            // Defined with string type to pass the command parameter
            private RelayCommand<string> _loadModuleCommand;
            //
            public RelayCommand<string> LoadModuleCommand
            {
                get
                {
                    if (_loadModuleCommand == null)
                    {
                        // Parameter 1: define a delegate method to be executed
                        // Parameter 2: the delegate function CanExedute
                        // with one-in(object) and one-out(bool) parameters
                        // to make the command enabled or disabled
                        _loadModuleCommand = new RelayCommand<string>(
                                      OnLoadModuleCommand,
                                      moduleId => moduleId != null);
                    }
                    return _loadModuleCommand;
                }
            }
            //
            private void OnLoadModuleCommand(String moduleId)
            {
                string xapUri;
                try
                {
                    if (_currentViewText != moduleId)
                    {
                        // For loading Xap or View
                        switch (moduleId)
                        {
                            // Load ProductApp.Xap on-demand
                            case ModuleID.ProductListView:
                                xapUri = "/ClientBin/ProductApp.Views.xap";
                                _catalogService.AddXap(xapUri, arg => ProductApp_OnXapDownloadCompleted(arg));
                                break;
                            // Add for other xaps or modules here
                            default:
                                throw new NotImplementedException();
                        }
                    }
                }
                catch (Exception ex)
                {
                    // Pass error object to View code-behind
                    Messenger.Default.Send(ex, MessageToken.RaiseErrorMessage);
                }
            }
    
            private void ProductApp_OnXapDownloadCompleted(AsyncCompletedEventArgs e)
            {
                // Send message back to View code-behind to load ProductList View
                Messenger.Default.Send(ModuleID.ProductListView, MessageToken.LoadScreenMessage);
    
                // Cache for check repeat commands later
                _currentViewText = ModuleID.ProductListView;
            }
        }
    }
  4. 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.

    XML
    <HyperlinkButton x:Name="linkButton_ProductList"
                     Style="{StaticResource LinkStyle}"
                     Content="Product List"
                     Command="{Binding Path=LoadModuleCommand}"
                     CommandParameter="ProductListView" />
  5. 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.

    C#
    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();
    
                // Register MVVMLight message handers
                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)
                {
                    // Import the ViewModel module into the View DataContext so that 
                    // the members of the ViewModel can be exposed
                    DataContext = _catalogService.GetModule(ModuleID.MainPageViewModel);
                }
            }
    
            // Method to be executed when receiving the message
            private void OnLoadScreenMessage(string moduleId)        {
                
                object newScreen;
                try
                {
                    // Import selected View module and then
                    // set the commandArg for UI changes
                    switch (moduleId)
                    {
                        case ModuleID.ProductListView:
                            newScreen = _catalogService.GetModule(ModuleID.ProductListView);                        
                            break; 
                            // Add for other modules here
                        default:
                            throw new NotImplementedException();
                    }
                    // Set the new screen
                    MainContent.Content = newScreen;
                    // UI - Set link button state
                    SetLinkButtonState(moduleId);
                }
                catch (Exception ex)
                {
                    OnRaiseErrorMessage(ex);
                }
            }
    
            // UI
            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)
            {
                // Error message display
                ChildWindow errorWin = new ErrorWindow(ex.Message, ex.StackTrace);
                errorWin.Show();
            }
    	
            private void OnUseDialogMessage(DialogMessage dialogMessage)
            {
                // MVVMLight DialogMessage callback processes
                if (dialogMessage != null)
                {
                    MessageBoxResult result = MessageBox.Show(dialogMessage.Content,
                    dialogMessage.Caption, dialogMessage.Button);
                    dialogMessage.ProcessCallback(result);
                }
            }        
        }
    }
  6. 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.

  1. Add a virtual folder ProductApp.Client into the solution and drag/drop the existing ProductApp.Views to the virtual folder.

  2. 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.

    .png

  3. Add the reference of the ProductApp.Common project into the two new projects.

  4. 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.

  5. Add some folders and files required for the data operations mainly performed in the ProductApp.Models project as shown below.

    24.png

    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.

    C#
    using System;
    using System.ComponentModel;
    using GalaSoft.MvvmLight;
    using ProductRiaLib.Web.Models;
    using ProductRiaLib.Web.Services;
    
    namespace ProductApp.Common
    {
        public interface IProductListModel : INotifyPropertyChanged, ICleanup
        {
            // Exposed data operation method and event handler pairs
            void GetCategoryLookup();
            event EventHandler<QueryResultsArgs<Category>> GetCategoryLookupComplete;
            void GetCategorizedProducts(int categoryId);
            event EventHandler<QueryResultsArgs<Product>> GetCategorizedProductsComplete;
            void SaveChanges(string operationType);
            event EventHandler<SubmitOperationArgs> SaveChangesComplete;        
            
            // Other exposed methods and properties
            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.

    C#
    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.

    C#
    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.

  6. 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.

    C#
    [Export(ModuleID.ProductListModel, typeof(IProductListModel))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class ProductListModel : IProductListModel
    {
        private ProductDomainContext _ctx;
            
        // Define event handlers for data operations
        public event EventHandler<QueryResultsArgs<Category>> GetCategoryLookupComplete;
        public event EventHandler<QueryResultsArgs<Product>> GetCategorizedProductsComplete;
        public event EventHandler<SubmitOperationArgs> SaveChangesComplete;
            
        // Remaining code - - -
    }

    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.

  7. 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.

    C#
    public ProductListViewModel() // Constructor
    {
        // Inject the Model as the Lazy object with the defined type
        _productListModel = ModuleCatalogService.Container.GetExport<IProductListModel>(ModuleID.ProductListModel).Value;
               
        // Register the event handlers defined in the Model            
        _productListModel.GetCategoryLookupComplete += ProductListModel_GetCategryComplete;
           
        // - - -
    } 
    
    private void ProductListModel_GetCategryComplete(object sender, QueryResultsArgs<Category> e)
    {
        if (!e.HasError)
        {
            // Set the returned data from async loading to data object
            CategoryItems = e.Results;                
                    
            if (SelectedCategory == null)
            {
                // Add the combo default item "Please Select"
                CategoryItems.Insert(0, _comboDefault);
    
                // Set selected category if not from state cache
                SelectedCategory = CategoryItems[0];                    
            }
        }
        else
        {
            // Send error message
            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.

    C#
    public RelayCommand CategorySelectionChanged
    {
        // - - -
    }
    //
    private void OnCategorySelectionChanged()
    {            
        // Reload the data to context based on the selected category
        Category item = SelectedCategory;
        if (item != null && item.CategoryID > 0)
        {
            _productListModel.GetCategorizedProducts((int)item.CategoryID);                    
        }
    
        // Remove the "Please Select"
        if (SelectedCategory != _comboDefault)
        {
            CategoryItems.Remove(_comboDefault);
        }
    
        // Enable the Add button
        AddProductCommand.RaiseCanExecuteChanged();            
    }
    
    public RelayCommand DataGridSelectionChanged
    {
        // - - - 
    }
    //
    private void OnDataGridSelectionChanged()
    {
        // Enable buttons                
        SaveChangesCommand.RaiseCanExecuteChanged();
        DeleteProductCommand.RaiseCanExecuteChanged();
    }
  8. Now update the code in the ProductList.xaml.cs code-behind. Only very simple code lines are placed in the class.

    C#
    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)
                {
                    // Set the DataContext to the imported ViewModel
                    DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.ProductListViewModel);
                }
            }
        }
    }
  9. 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.

    XML
    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.

    XML
    <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.

  1. 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.

    C#
    public AddProductWindowViewModel() // Constructor
    {
         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.

    C#
    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.

    C#
    public ProductListViewModel() // Constructor
    {            
        Messenger.Default.Register(this, MessageToken.DataToProductListVmMessage, 
                  new Action<Product>(OnDataToProductListVmMessage));
    }
    
    private void OnDataToProductListVmMessage(Product addedProduct)
    {	    		
        if (!_productListModel.IsBusy && addedProduct != null)
        {
            // Add to Context in the model
            _productListModel.AddNewProduct(addedProduct);
    
            // Add to ObCollection for refreshing display
            ProductItems.Add(addedProduct);
        }
    }
  2. 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.

    C#
    public AddProductWindow() // Constructor
    {
        InitializeComponent();
        Messenger.Default.Register(this, MessageToken.AddProductWindowMessage, 
                  new Action<string>(OnAddProductWindowMessage));
    
        if (!ViewModelBase.IsInDesignModeStatic)
        {
            // Set the DataContext to the imported ViewModel
            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;
        }
    }
  3. 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.

  4. Add code lines to the parent View ProductList.xaml.cs code-behind for loading and opening the child window.

    C#
    private ChildWindow _addProdScreen;
    
    public ProductList() // Constructor
    {
        // Register the massage handler for loading child window
        Messenger.Default.Register(this, MessageToken.LoadAddProductViewMessage,
                            new ActionA<string>(OnLoadAddProductViewMessage));
        
        // - - - Other code lines
    }
    
    private void OnLoadAddProductViewMessage(string message)
    {
        // Load AddProductWindowView lazy module
        _addProdScreen = _catalogService.GetModuleLazy(ModuleID.AddProductWindowView) as ChildWindow;
        _addProdScreen.Show();
    }
  5. 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.

    25.png

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Shenwei is a software developer and architect, and has been working on business applications using Microsoft and Oracle technologies since 1996. He obtained Microsoft Certified Systems Engineer (MCSE) in 1998 and Microsoft Certified Solution Developer (MCSD) in 1999. He has experience in ASP.NET, C#, Visual Basic, Windows and Web Services, Silverlight, WPF, JavaScript/AJAX, HTML, SQL Server, and Oracle.

Comments and Discussions

 
-- There are no messages in this forum --