This project is read-only.

Real-World MVVM with WCF RIA Services

The Simple MVVM Toolkit is entirely compatible with WCF RIA Services, which provides a WCF service that can perform CRUD (Create, Retrieve, Update, Delete) operations, and well as custom queries and updates. You can use Linq to Entities right out of the box, or create a domain service that interacts with a Data Access Layer using another ORM (object-relational mapper) such as NHibernate.  RIA Services allows you to more rapidly develop Silverlight line of business apps with features you would otherwise have to write yourself, such as change-tracking for batch updates, end-to-end validation (including async validation), and authentication / authorization.  For more information take a look at my screencast on WCF RIA Services.

Download the completed project here.  View the screencast for this tutorial:

Real-World MVVM Part 1 Streaming or Download (PC or mobile device)
Real-World MVVM Part 2 Streaming or Download (PC or mobile device)

The easiest way to build a Silverlight MVVM app that uses RIA Services is to use the Visual Studio project template installed by the Simple MVVM Toolkit. Before getting started, be sure that you have installed the required prerequisites, including the Blend SDK, SQL Express and the Northwind sample database (which you must attach to the SQL Express instance).

  1. Open Visual Studio 2010, then select File, New Project.

    mvvm-ria-proj

    • Under the Silverlight category, select the Mvvm sub-category.
    • Select the SimpleMvvmRiaService project template.
  2. You will get a solution with three projects: a Silverlight client, an ASP.NET web host, and a Silverlight Test project.

    smvvm-ria

    • The Silverlight project includes a reference to the SimpleMvvmToolkit assembly,
    • If references for Microsoft.Expression.Interactions and System.Windows.Interactivity are missing,
      you need to install the Microsoft Expression Blend SDK.
  3. If you press F5 to run the app, you will  see a functioning Silverlight MVVM app that talks to WCF RIA Services.

    smvvm-riarun

    • The Load, Add, Edit and Remove buttons are functional, with Add and Edit displaying a model dialog and Remove prompting the user for confirmation.
    • The Load button executes a LoadItems method on the ItemListViewModel, which obtains items from a MockItemListServiceAgent.
  4. Set the Test project as the Startup project, then press F5 to execute unit tests against the ItemListViewModel.

    smvvm-testrun

  5. Now that you know what you get out of the box with the SimpleMvvmRiaServices project template, it’s time to incorporate your own classes.

    edmx

    • We’re going to start by adding a Models folder to the Web project.
    • Right-click on the Models folder, select Add New Item, then select a new ADO.NET Entity Data Model.
    • We’re going to be using the Northwind database, so name it Northwind.edmx.
  6. Select a data connection to the Northwind database (or create a new one).

    data-connection

    • Bring in both the Categories and Products tables.
    • Then remove Description and Picture properties from Category.
    • Also remove SupplierID, QuantityPerUnit, UnitsInStock, UnitsOnOrder,  and ReorderLevel properties from Product.
    • Before proceeding further you need to build the Web project.

    cat-prod

  7. Now it’s time to add a domain service to the Web project.  This will generate classes based on the data model, which are then projected to the client.
    • Right-click on the Services folder and select Add New Item.
    • Type Domain Service in the search box and select Domain Service Class.
    • Name the class NorthwindDomainService.

    domain-svc

    • In the Add Domain Service dialog, check both Category and Product entities.
    • Enable editing of Products and leave the option checked to generate associated metadata.

    add-domain-svc

  8. You may wish to run this wizard again in the future, so I recommend adding the “partial” keyword to  the NorthwindDomainService class.
    • Right-click on the Services folder to add a class named MyNorthwinDomainService.cs, then remove “My” from the class name and make it a partial class.
    • Add a method called GetProductsByCategory to the second MyNorthwinDomainService.cs that accepts a categoryId parameter (int).
    • Return a query that filters products by the specified category and sorts them by ProductName.
    • Insert the Include operator before the where clause, specifying the “Category” property (so that we can show CategoryName).
    public partial class NorthwindDomainService
    {
        public IQueryable<Product> GetProductsByCategory(int categoryId)
        {
            var query = from p in this.ObjectContext.Products
                            .Include("Category")
                        where p.CategoryID == categoryId
                        orderby p.ProductName
                        select p;
            return query;
        }
    }
    • Next, you will need to open NorthwindDomainService.metadata.cs, go to the ProductMetadata class and add an Include attribute to the Category property.
    internal sealed class ProductMetadata
    {
        private ProductMetadata() { }
    
        [Include]
        public Category Category { get; set; }
    
        // Other propertied elided for clarity ...
    }
    • If you build the project and show all files for the Silverlight project, you will see a Generated_Code folder that includes a file called SimpleMvvmRiaServices.Web.g.cs.
    • In this file you will find a NorthwindDomainContext class as well as Category and Product entity classes.
  9. Now it’s time to work on the Silverlight project.  Right-click on the Services folder to add an interface called IProductServiceAgent.
    • Using IItemListServiceAgent as an example, add methods to retrieve, add and remove entities, as well as save and reject changes.
    • Because of the asynchronous nature of invoking services in Silverlight, you should include a callback Action parameter to harvest results or an exception.
    • Add, Remove and RejectChanges methods are local and do not require a callback parameter.
    public interface IProductServiceAgent
    {
        // Retrieve categories
        void GetCategories(Action<List<Category>, Exception> completed);
    
        // Retrieve products
        void GetProductsByCategory(int categoryId, 
            Action<List<Product>, Exception> completed);
    
        // Insert product
        void AddProduct(Product item);
    
        // Remove product
        void RemoveProduct(Product item);
    
        // Save changes
        void SaveChanges(Action<Exception> completed);
    
        // Reject changes
        void RejectChanges();
    }
  10. Using MockItemListServiceAgent as an example, implement the IProductServiceAgent interface in a MockProductServiceAgent class.
    • In order to support async invocation you will need to use a BackgroundWorker in the Get methods.
      • Wire up the BackgroundWorker DoWork event to return some fake entities
      • Wire up the BackgroundWorker Completed event to invoke the completed parameter
    • Include the ServiceAgentExport attribute with AgentType set to Mock.
    • Add fields for mockCategories and mockProducts and initialize them in the ctor.
    • For now do not implement SaveChanges or RejectChanges
    • Rename AddItem and RemoveItem to AddProduct and RemoveProduct
    // Add ServiceAgentExport attribute, setting AgentType to Mock
    [ServiceAgentExport(typeof(IProductServiceAgent), AgentType = AgentType.Mock)]
    public class MockProductServiceAgent : IProductServiceAgent
    {
        // Mock data
        List<Category> mockCategories;
        List<Product> mockProducts;
    
        public MockProductServiceAgent ()
        {
            // Init mock data
            mockCategories = new List<Category>
            {
                new Category { CategoryID = 1, CategoryName = "Beverages" },
                new Category { CategoryID = 2, CategoryName = "Condiments" },
                new Category { CategoryID = 3, CategoryName = "Confections" }
            };
    
            // Mock products
            mockProducts = new List<Product>
            {
                new Product { ProductID = 1, ProductName = "Latte", CategoryID = 1,
                    Category = mockCategories[0] },
                new Product { ProductID = 2, ProductName = "Capuccino", CategoryID = 1,
                    Category = mockCategories[0] },
                new Product { ProductID = 3, ProductName = "Mustard", CategoryID = 2,
                    Category = mockCategories[1] },
                new Product { ProductID = 4, ProductName = "Cake", CategoryID = 3,
                    Category = mockCategories[2] }
            };
        }
    
        // Get items asynchonously using BackgroundWorker
        public void GetCategories(Action<List<Category>, Exception> completed)
        {
            // Use background worker to simulate async operation
            var bw = new BackgroundWorker();
    
            // Handle DoWork event to perform task on a background thread
            bw.DoWork += (s, ea) =>
                {
                    // Simulate work by sleeping
                    Thread.Sleep(TimeSpan.FromSeconds(2));
    
                    // Set result to mock categories
                    ea.Result = mockCategories;
                };
    
            // Handle RunWorkerCompleted event by invoking completed callback
            bw.RunWorkerCompleted += (s, ea) =>
                {
                    if (ea.Error != null)
                        completed(null, ea.Error);
                    else
                        completed((List<Category>)ea.Result, null);
                };
    
            // Call RunWorkerAsync to begin operation
            bw.RunWorkerAsync();
        }
    
        // Get items asynchonously using BackgroundWorker
        public void GetProductsByCategory(int categoryId, Action<List<Product>, 
            Exception> completed)
        {
            // Use background worker to simulate async operation
            var bw = new BackgroundWorker();
    
            // Handle DoWork event to perform task on a background thread
            bw.DoWork += (s, ea) =>
            {
                // Simulate work by sleeping
                Thread.Sleep(TimeSpan.FromSeconds(2));
    
                // Set result to mock categories
                ea.Result = mockProducts;
            };
    
            // Handle RunWorkerCompleted event by invoking completed callback
            bw.RunWorkerCompleted += (s, ea) =>
            {
                if (ea.Error != null)
                    completed(null, ea.Error);
                else
                    completed((List<Product>)ea.Result, null);
            };
    
            // Call RunWorkerAsync to begin operation
            bw.RunWorkerAsync();
        }
    
        // Add item
        public void AddProduct(Product item)
        {
            mockProducts.Add(item);
        }
    
        // Remove item
        public void RemoveProduct(Product item)
        {
            mockProducts.Remove(item);
        }
    
        // TODO: Implement mock saves (optional)
        public void SaveChanges(Action<Exception> completed)
        {
            MessageBox.Show("SaveChanges not implemented");
        }
    
        // TODO: Implement mock change rejection (optional)
        public void RejectChanges()
        {
            MessageBox.Show("RejectChanges not implemented");
        }
    }
  11. Now that you’ve created a mock service agent, you need to add a real one that interacts with the Domain Context class.
    • Using ItemListServiceAgent as an example, create a ProductServiceAgent class that implements IProductServiceAgent
    • Add a fields that is initialized to a new NorthwindDomainContext.
    • In each of the “get” methods, GetCategories and GetProducts, call Load on the domain context, passing the query factory method
      • In the callback set the load operation’s Error or Entities property and then invoke the completed callback.
    • Rename AddItem and RemoveItem methods to AddProduct and RemoveProduct and uncomment the code
    • Uncomment code for SaveChanges and RejectChanges methods
    // Add ServiceAgentExport attribute, setting AgentType to Mock
    [ServiceAgentExport(typeof(IProductServiceAgent), AgentType = AgentType.Real)]
    public class ProductServiceAgent : IProductServiceAgent
    {
        // Add a field of type NorthwindDomainContext
        NorthwindDomainContext domainContext = new NorthwindDomainContext();
    
        // Load categories from domain context
        public void GetCategories(Action<List<Category>, Exception> completed)
        {
            // Load GetCategoriesQuery
            EntityQuery<Category> query = domainContext.GetCategoriesQuery();
            domainContext.Load(query, loadOp =>
            {
                // Declare error and result
                Exception error = null;
                IEnumerable<Category> items = null;
    
                // Set error or result
                if (loadOp.HasError)
                {
                    error = loadOp.Error;
                }
                else
                {
                    items = loadOp.Entities;
                }
    
                // Invoke completion callback
                completed(items.ToList(), error);
            }, null);
        }
    
        // Load products from domain context
        public void GetProductsByCategory(int categoryId, Action<List<Product>, 
            Exception> completed)
        {
            // Load GetProductsByCategoryQuery
            EntityQuery<Product> query = domainContext
                .GetProductsByCategoryQuery(categoryId);
            domainContext.Load(query, loadOp =>
            {
                // Declare error and result
                Exception error = null;
                IEnumerable<Product> items = null;
    
                // Set error or result
                if (loadOp.HasError)
                {
                    error = loadOp.Error;
                }
                else
                {
                    items = loadOp.Entities;
                }
    
                // Invoke completion callback
                completed(items.ToList(), error);
            }, null);
        }
    
        // Call Add on domain context items
        public void AddProduct(Product item)
        {
            domainContext.Products.Add(item);
        }
    
        // Call Remove on domain context items
        public void RemoveProduct(Product item)
        {
            domainContext.Products.Remove(item);
        }
    
        // Save changes on the domain content if there are any
        public void SaveChanges(Action<Exception> completed)
        {
            // See if any products have changed
            if (domainContext.Products.HasChanges)
            {
                // Submit bulk update
                domainContext.SubmitChanges(submitOp =>
                {
                    // Declare error
                    Exception error = null;
    
                    // Set error or result
                    if (submitOp.HasError)
                    {
                        error = submitOp.Error;
                    }
    
                    // Invoke completion callback
                    completed(error);
                }, null);
            }
        }
    
        // Reject unsaved changes on domain context
        public void RejectChanges()
        {
            if (this.domainContext.Products.HasChanges)
            {
                this.domainContext.RejectChanges();
            }
        }
    }
  12. With the mock and real service agents complete, you need to add a ViewModel for products.
    • Right-click on the ViewModels folder and select Add New Item
      • Expand the Silverlight / Mvvm category, then select SimpleMvvmViewModel
      • Name it ProductListViewModel

    prodlist-vm

    • Replace the IXxxServiceAgent comment with IProductServiceAgent 
    • Using ItemListViewModel as an example, add event handlers for SaveChanges and ItemsSaved
    public event EventHandler<NotificationEventArgs<object, bool>> SaveChangesNotice;
    public event EventHandler<NotificationEventArgs> ItemsSavedNotice;
    • Using the mvvmprop code snippet,  add a Categories property (ObservableCollection<Category>) and a SelectedCategory property.
      • Notice how the snippet uses a lambda expression for firing the PropertyChanged event in a type-safe manner
    • Using the mvvmprop code snippet,  also add a Products property (ObservableCollection<Product>) and a SelectedProduct property.
    private ObservableCollection<Category> categories;
    public ObservableCollection<Category> Categories
    {
        get { return categories; }
        set
        {
            categories = value;
            NotifyPropertyChanged(vm => vm.Categories);
        }
    }
    
    private Category selectedCategory;
    public Category SelectedCategory
    {
        get { return selectedCategory; }
        set
        {
            selectedCategory = value;
            NotifyPropertyChanged(vm => vm.SelectedCategory);
        }
    }
    
    
    private
    ObservableCollection<Product> products; public ObservableCollection<Product> Products { get { return products; } set { products = value; NotifyPropertyChanged(vm => vm.Products); } } private Product selectedProduct; public Product SelectedProduct { get { return selectedProduct; } set { selectedProduct = value; NotifyPropertyChanged(vm => vm.SelectedProduct); } }
  13. Use the mvvmprop code snippet to add the following properties:
    • IsBusy, CanLoad, CanAdd, CanEdit, CanRemove
    private bool isBusy;
    public bool IsBusy
    {
        get { return isBusy; }
        set
        {
            isBusy = value;
            NotifyPropertyChanged(m => m.IsBusy);
        }
    }
    
    private bool canLoad = true;
    public bool CanLoad
    {
        get { return canLoad; }
        set
        {
            canLoad = value;
            NotifyPropertyChanged(m => m.CanLoad);
        }
    }
    
    private bool canAdd;
    public bool CanAdd
    {
        get { return canAdd; }
        set
        {
            canAdd = value;
            NotifyPropertyChanged(m => m.CanAdd);
        }
    }
    
    private bool canEdit;
    public bool CanEdit
    {
        get { return canEdit; }
        set
        {
            canEdit = value;
            NotifyPropertyChanged(m => m.CanEdit);
        }
    }
    
    private bool canRemove;
    public bool CanRemove
    {
        get { return canRemove; }
        set
        {
            canRemove = value;
            NotifyPropertyChanged(m => m.CanRemove);
        }
    }
    • Copy over the SetCanProperties helper method
      • Invoke it from the SelectedProduct and IsBusy property setters
    private void SetCanProperties()
    {
        CanLoad = !IsBusy;
        CanAdd = !IsBusy;
        CanEdit = !IsBusy && SelectedProduct != null;
        CanRemove = !IsBusy && SelectedProduct != null;
    }
  14. Add a LoadCategories method to the ViewModel, invoking GetCategories on the serviceAgent.
    • In the completion callback invoke a CategoriesLoaded method to set Categories and SelectedCategory properties
      • For an example, see LoadItems and ItemsLoaded methods in ItemListViewModel.cs.
    • Repeat this for a LoadProducts method, calling GetProductsByCategory on the serviceAgent.
      • Call a ProductsLoaded method in the completion callback.
    • Repeat this for a SaveChanges method, calling SaveChanges on the serviceAgent.
      • Notify the view of pending changes and handle save in a callback
      • Call a ItemsSaved method in the completion callback.
      • In ItemsSaved method notify view that items were successfully saved or that there was an error
    public void LoadCategories()
    {
        // Load items
        serviceAgent.GetCategories
            (
                (entities, error) => CategoriesLoaded(entities, error)
            );
    
        // Reset property
        Categories = null;
    
        // Flip busy flag
        IsBusy = true;
    }
    
    public void LoadProducts()
    {
        // Load items
        serviceAgent.GetProductsByCategory
            (
                SelectedCategory.CategoryID,
                (entities, error) => ProductsLoaded(entities, error)
            );
    
        // Reset property
        Categories = null;
    
        // Flip busy flag
        IsBusy = true;
    }
    
    
    public void SaveChanges()
    {
        // Prompt the user to save changes if there are any
        Notify(SaveChangesNotice, new NotificationEventArgs<object, bool>
            ("There are unsaved changes. Do you wish to save?", null,
            confirm =>
            {
                if (confirm)
                {
                    // Save changes
                    serviceAgent.SaveChanges
                        (error => ItemsSaved(error));
                }
            }));
    }
    
    private
    void CategoriesLoaded(List<Category> entities, Exception error) { // If no error is returned, set the model to entities if (error == null) { Categories = new ObservableCollection<Category>(entities); } // Otherwise notify view that there's an error else { NotifyError("Unable to retrieve items", error); } // Set SelectedItem to the first item if (Categories.Count > 0) { SelectedCategory = Categories[0]; } // We're done IsBusy = false; } private void ProductsLoaded(List<Product> entities, Exception error) { // If no error is returned, set the model to entities if (error == null) { Products = new ObservableCollection<Product>(entities); } // Otherwise notify view that there's an error else { NotifyError("Unable to retrieve items", error); } // Set SelectedItem to the first item if (Products.Count > 0) { SelectedProduct = Products[0]; } // We're done IsBusy = false; }
    private void ItemsSaved(Exception error)
    {
        if (error == null)
        {
            // Notify view products were saved successfully
            Notify(ItemsSavedNotice, new NotificationEventArgs
                ("Items were successfully saved"));
        }
        else
        {
            // Notify view if there's an error
            NotifyError("Unable to save items", error);
        }
    
        // We're done
        IsBusy = false;
    }
    • Also place Add, Remove and RejectChanges methods on the ProductListViewModel
      • Call corresponding methods on the serviceAgent
      • Use the methods in ItemListViewModel as an example, replacing Item(s) with Product(s)
      • In RejectChanges call RejectChanges on the serviceAgent, then call LoadProducts
    public void Add(Product item)
    {
        if (Products != null)
        {
            serviceAgent.AddProduct(item);
            Products.Add(item);
            SelectItem(item);
            SetCanProperties();
        }
    }
    
    public void Remove()
    {
        if (SelectedProduct != null)
        {
            serviceAgent.RemoveProduct(SelectedProduct);
            Products.Remove(SelectedProduct);
            SelectItem(null);
            SetCanProperties();
        }
    }
    
    public void RejectChanges()
    {
        serviceAgent.RejectChanges();
        LoadProducts();
    }
    • Make sure to build the Silverlight project and correct any compilation errors
  15. Open InjectedViewModelLocator.cs, found in the Locators folder in the Silverlight project
    • Using the mvvminjectedlocator code snippet add the ProductListViewModel as a property
      • Specify IProductListServiceAgent for the service agent interface
      • Uncomment the Debug.Assert statement and move it to the ctor in order to verify creation of the service agent
    • Also set the agentType field to AgentType.Real
    // Create ProductListViewModel on demand
    [ImportMany]
    public Lazy<IProductServiceAgent, IServiceAgentMetadata>[] ProductServiceAgents { get; set; }
    public ProductListViewModel ProductListViewModel
    {
        get
        {
            var serviceAgent = ProductServiceAgents
                .Where(sa => sa.Metadata.AgentType == agentType).FirstOrDefault();
            ProductListViewModel viewModel = null;
            if (serviceAgent != null) viewModel = new ProductListViewModel(serviceAgent.Value);
            else if (DesignerProperties.IsInDesignTool) viewModel = new ProductListViewModel();
            return viewModel;
        }
    }
  16. Now that you have the ProductListViewModel constructed, you can create a unit test for it in the Test project
    • Right-click on the Test project and select Add, New Item
    • Choose the Silverlight / Mvvm category and select SimpleMvvmViewModelTests
      • Name the file ProductListViewModelTests.cs

    prodlistvm-tests

    • Complete the TODO items to flesh out a LoadCategoriesTest method
      • In the ctor create the InjectedViewModelLocator and retrieve the ProductListViewModel
      • In the test method handle an error notice from the VM by failing the test
      • Handle the PropertyChanged event on Categories by completing the test
    • There are some helper methods in the SilverlightTest base class you call in order to perform the needed steps for a unit test
      • Call EnqueueCallback, executing LoadCategories on the ViewModel
      • Call this.EnqueueConditional, passing a 10 second timeout
      • Call EnqueueCallback to assert that the Categories property has been set
      • Call EnqueueTestComplete to terminate the unit test
    [TestClass]
    public class ProductListViewModelTests : SilverlightTest
    {
        // Add ViewModel field
        ProductListViewModel viewModel;
    
        // Initialize ViewModel
        public ProductListViewModelTests()
        {
            // Create locator
            var locator = new InjectedViewModelLocator();
            locator.AgentType = AgentType.Mock;
    
            // Get ViewModel
            this.viewModel = locator.ProductListViewModel;
            Assert.IsNotNull(viewModel, "ServiceAgent not injected for ViewModel");
        }
    
        [TestMethod, Asynchronous]
        public void LoadCategoriesTest()
        {
            // Completion flag
            bool completed = false;
    
            // Handle error
            viewModel.ErrorNotice += (s, ea) => Assert.Fail(ea.Data.Message);
    
            // Handle property change
            viewModel.PropertyChanged += (s, ea) =>
            {
                if (ea.PropertyName == "Categories"
                    && viewModel.Categories != null) completed = true;
            };
    
            // Call ViewModel method
            EnqueueCallback(() => viewModel.LoadCategories());
    
            // Wait for completion with timeout
            int timeoutSeconds = 10; // Debugging: Timeout.Infinite
            this.EnqueueConditional(() => completed, timeoutSeconds);
    
            // Perform Asserts
            EnqueueCallback(() =>
                {
                    Assert.IsNotNull(viewModel.Categories);
                });
    
            // Complete test
            EnqueueTestComplete();
        }
    }
    • Set the Test project as the startup project and press F5 to run it. All tests should pass.
      • Then reset the Web project as the startup project

    cat-test

  17. Last but not least, we are going to add a ProductListView to the Silverlight project.
    • Right-click on the Views folder, select Add New Item, then choose Silverlight Page
      • Name the file ProductListView.cs

    prodlist-view

    • To use the XML snippets installed by the toolkit you need to open the view using the XML Text Editor
      • Right-click on ProductListView.xaml and select Open With, then choose XML Editor from the list
      • First place the cursor in the xml namespaces section, then right-click and select Inert Snippet
      • Select the mvvmxmlns snippet to insert the Blend namespaces
      • Then use the mvvmcontext snippet to bind the page’s DataContext to the ProductListViewModel property of the ViewModelLocator
      • Use the mvvmtrigger snippet to insert an event trigger for the Loaded event on the page to call the LoadCategories method
      <!--EndFragment-->
    • The remaining effort for the ViewModel consists of laying out some nested StackPanels with a combo box, buttons and a DataGrid
      • Using ItemListView.xaml as an example, change the root Grid to a StackPanel, then insert a nested horizontal StackPanel
      • Insert a combobox with its ItemsSource bound to Categories and SelectedItem bound to SelectedCategory (Mode=TwoWay)
      • Set DisplayMemberPath on the combobox to to CategoryName
      • Copy xaml for the Load, Add, Edit, Remove, SaveChanges and RejectChanges buttons
      • Make sure to set IsEnabled to True on the SaveChanges and RejectChanges buttons
      • Modify the event trigger on the Load button to call the LoadProducts method
    • Drag a DataGrid from the toolbox to a place beneath the StackPanel containing the combobox and buttons
      • Bind ItemsSource and SelectedItem to Products and SelectedProduct (Mode=TwoWay)
      • Add column bound to Product properties
    • Copy the <Grid> element with a BusyIndicator from ItemListView.xaml to beneath the DataGrid
    • Below is the resulting XAML for the ProductListView. Highlighted sections show inserts from XML snippets.
    <navigation:Page x:Class="SimpleMvvmRiaServices.Views.ProductListView" 
               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:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
               xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
               xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"
               xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
               xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
               mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
               Title="Products"
               DataContext="{Binding Source={StaticResource Locator}, Path=ProductListViewModel}" >
        <!-- Load categories to populate the combobox -->
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="Loaded">
            <ei:CallMethodAction
                    TargetObject="{Binding}"
                    MethodName="LoadCategories"/>
          </i:EventTrigger>
        </i:Interaction.Triggers>
    
      <!-- Button Style -->
        <navigation:Page.Resources>
            <Style TargetType="Button">
                <Style.Setters>
                    <Setter Property="Height" Value="23"/>
                    <Setter Property="Width" Value="60"/>
                    <Setter Property="Margin" Value="5,0,0,0"/>
                </Style.Setters>
            </Style>
        </navigation:Page.Resources>
    
        <StackPanel x:Name="LayoutRoot">
            <StackPanel Orientation="Horizontal">
                <ComboBox Height="23" Name="categoriesComboBox" Width="120"
                          ItemsSource="{Binding Path=Categories}"
                          SelectedItem="{Binding Path=SelectedCategory, Mode=TwoWay}"
                          DisplayMemberPath="CategoryName"/>
                <Button Name="loadButton" 
                        Content="Load"
                        IsEnabled="{Binding CanLoad}">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:CallMethodAction 
                                TargetObject="{Binding}"
                                MethodName="LoadProducts"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <Button Name="addButton"
                        Content="Add"
                        IsEnabled="{Binding CanAdd}" Click="addItemButton_Click" />
                <Button Name="editButton"
                        Content="Edit" 
                        IsEnabled="{Binding CanEdit}" Click="editItemButton_Click" />
                <Button Name="removeButton"
                        Content="Remove" 
                        IsEnabled="{Binding CanRemove}" Click="removeItemButton_Click" />
                <!-- Set IsEnabled to True -->
                <Button Name="saveChangesButton"
                        Content="Save Changes" 
                        IsEnabled="True" Width="94">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:CallMethodAction 
                                TargetObject="{Binding}"
                                MethodName="SaveChanges"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <!-- Set IsEnabled to True -->
                <Button Name="rejectChangesButton"
                        Content="Reject Changes" 
                        IsEnabled="True" Width="94">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:CallMethodAction 
                                TargetObject="{Binding}"
                                MethodName="RejectChanges"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
            </StackPanel>
            <sdk:DataGrid AutoGenerateColumns="False" Name="productsDataGrid" IsReadOnly="True"
                          ItemsSource="{Binding Path=Products}"
                          SelectedItem="{Binding Path=SelectedProduct, Mode=TwoWay}">
                <sdk:DataGrid.Columns>
                    <sdk:DataGridTextColumn Binding="{Binding Path=ProductID}" CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" Header="Product Id" />
                    <sdk:DataGridTextColumn Binding="{Binding Path=ProductName}" CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" Header="Product Name" />
                    <sdk:DataGridTextColumn Binding="{Binding Path=UnitPrice}" CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" Header="Unit Price" />
                    <sdk:DataGridTextColumn Binding="{Binding Path=Category.CategoryName}" CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" Header="Category" />
                    <sdk:DataGridCheckBoxColumn Binding="{Binding Path=Discontinued}" CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" Header="Discontinued" />
                </sdk:DataGrid.Columns>
            </sdk:DataGrid>
            <Grid>
                <!-- Bind IsBusy to IsBusy -->
                <toolkit:BusyIndicator Name="isBusyIndicator" Height="80" Width="200" 
                    IsBusy="{Binding IsBusy}" Margin="152,39,148,-39" />
            </Grid>
        </StackPanel>
    </navigation:Page>
    • Add, Edit and Remove buttons have Click event handlers. To insert the code behind methods, right-click on each handler in the XAML and select Navigate To EventHandler
      • Add a field for ProductListViewModel and grab it from the DataContext in the code-behind ctor
      • In the ctor also subscribe to notifications from the ViewModel for ErrorNotice, SaveChangesNotice and ItemsSavedNotice
      • Paste code for the handler methods from ItemListView.xaml.cs
    public partial class ProductListView : Page
    {
        // ViewModel
        ProductListViewModel viewModel;
    
        public ProductListView()
        {
            InitializeComponent();
    
            // Get vm from data context
            viewModel = (ProductListViewModel)DataContext;
    
            // Wire up handlers for vm notifications
            viewModel.ErrorNotice += OnErrorNotice;
            viewModel.SaveChangesNotice += OnSaveChangesNotice;
            viewModel.ItemsSavedNotice += OnItemsSavedNotice;
        }
    
        void OnErrorNotice(object sender, NotificationEventArgs<Exception> e)
        {
            // Show user message string
            MessageBox.Show(e.Message, "Error", MessageBoxButton.OK);
    
            // Trace information
            Debug.WriteLine(e.Data.ToString());
        }
    
        void OnSaveChangesNotice(object sender, NotificationEventArgs<object, bool> e)
        {
            // Prompt user to save changes
            var mbResult = MessageBox.Show("Save changes?", "Save Changes", MessageBoxButton.OKCancel);
    
            // Call back ViewModel with response
            e.Completed(mbResult == MessageBoxResult.OK);
        }
    
        void OnItemsSavedNotice(object sender, NotificationEventArgs e)
        {
            // Inform user product was saved
            MessageBox.Show(e.Message, "Save Changes", MessageBoxButton.OK);
        }
            
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
        }
    
        private void addItemButton_Click(object sender, RoutedEventArgs e)
        {
        }
    
        private void editItemButton_Click(object sender, RoutedEventArgs e)
        {
        }
    
        private void removeItemButton_Click(object sender, RoutedEventArgs e)
        {
        }
    }
  18. Edit MainPage.xaml by setting the CommandParameter of the items hyperlink button to ProductListView
    • If you run the app and click the button the ProductListView should appear
    • Select a category and click the Load button. Products for that category should fill the data grid.

    prods-runtime

  19. Now we’re going to create a ProductDetailViewModel with a corresponding ProductDetailView for adding and editing an individual product.
    •   Right-click on the ViewModels folder and select Add New Item.
    • Then expand the Silverlight/ Mvvm category and select SimpleMvvmViewModelDetail
      • Name it ProductDetailViewModel

    proddetail-vm

    • Replace the /* DetailType */ comment with Product, both in the class type argument and in the ctor
      • Remove the serviceAgent field and the ctor that accepts serviceAgent
    • Use the mvvmprop code snippet to insert a Categories property (ObservableCollection<Category>)
    public class ProductDetailViewModel : ViewModelDetailBase<ProductDetailViewModel, Product>
    {
        #region Initialization and Cleanup
    
        // Default ctor
        public ProductDetailViewModel() { }
    
        // Ctor to set base.Model to DetailType
        public ProductDetailViewModel(Product model)
        {
            base.Model = model;
        }
    
        #endregion
    
        #region Notifications
    
        // Add events to notify the view or obtain data from the view
        public event EventHandler<NotificationEventArgs<Exception>> ErrorNotice;
    
        #endregion
    
        #region Properties
    
        // Add properties using the mvvmprop code snippet
    
        private ObservableCollection<Category> categories;
        public ObservableCollection<Category> Categories
        {
            get { return categories; }
            set
            {
                categories = value;
                NotifyPropertyChanged(vm => vm.Categories);
            }
        }
    
        #endregion
  20. Add a new Silverlight  Child Window to the Views folder and name it ProductDetailView.xaml

    prod-detail-v

    • Copy the label and textbox styles from ItemDetailView.xaml.
    • Copy the nested Grid from ItemDetailView.xaml and paste it above the buttons.
    • Add three rows to the Grid, for a total of five rows
    • Specify 5 labels: Product ID, Product Name, Category, Unit Price, Discontinued
    • Specify corresponding textboxes bound to Model.ProductID, Model.ProductName and Model.UnitPrice
    • Insert a combobox and name it categoriesComboBox
      • Bind ItemsSource the Categories property
      • Set DisplayMemberPath to CategoryName
      • Bind SelectedValue to Model.CategoryID
      • Set SelectedValuePath to CategoryID
    <controls:ChildWindow x:Class="SimpleMvvmRiaServices.Views.ProductDetailView"
               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
               xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
               xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
               Width="300" Height="260" 
               Title="Product" >
    
        <!-- Label and TextBox styles -->
        <sdk:ChildWindow.Resources>
            <Style TargetType="sdk:Label">
                <Style.Setters>
                    <Setter Property="Height" Value="28"/>
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="Margin" Value="10,0,0,0"/>
                </Style.Setters>
            </Style>
            <Style TargetType="TextBox">
                <Style.Setters>
                    <Setter Property="HorizontalAlignment" Value="Left"/>
                    <Setter Property="Margin" Value="5"/>
                </Style.Setters>
            </Style>
        </sdk:ChildWindow.Resources>
        
        <Grid x:Name="LayoutRoot" Margin="2">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
    
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="136*" />
                    <ColumnDefinition Width="242*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                
                <sdk:Label Content="Product ID:" Grid.Row="0"/>
                <sdk:Label Content="Product Name:" Grid.Row="1" />
                <sdk:Label Content="Category:" Grid.Row="2" />
                <sdk:Label Content="Unit Price:" Grid.Row="3" />
                <sdk:Label Content="Discontinued:" Grid.Row="4" />
    
                <!-- Bind controls to Model properties -->
                <TextBox Name="productId" Text="{Binding Path=Model.ProductID, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="0" Height="28" Width="150"
                         IsReadOnly="True" Background="LightGray" />
                <TextBox Name="productName" Text="{Binding Path=Model.ProductName, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="1" Height="28" Width="150"/>
                <TextBox Name="unitPrice" Text="{Binding Path=Model.UnitPrice, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="3" Height="28" Width="150"/>
                <CheckBox Name="discontinued" IsChecked="{Binding Path=Model.Discontinued, Mode=TwoWay}"
                          Height="16" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="1" Grid.Row="4" />
                <ComboBox Name="categoryComboBox" Grid.Column="1" Grid.Row="2" Margin="5,5,5,5"
                          Height="25" HorizontalAlignment="Left" VerticalAlignment="Top" Width="150"
                          ItemsSource="{Binding Path=Categories}"
                          DisplayMemberPath="CategoryName" 
                          SelectedValue="{Binding Path=Model.CategoryID, Mode=TwoWay}"
                          SelectedValuePath="CategoryID"/>
            </Grid>
            
            <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" 
                    Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />
            <Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" 
                    Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1" />
        </Grid>
    </controls:ChildWindow>
    • Open the code-behind for ProductDetailView and add a ctor that accepts ProductDetailViewModel
      • Set DataContext to the incoming ProductDetailViewModel
      • Invoke the default ctor
    // Set DataContext to ProductDetailViewModel
    public ProductDetailView(ProductDetailViewModel viewModel)
        : this()
    {
        // Set data context to view model
        DataContext = viewModel;
    }
  21. Starting at line 70 in ItemListView.xaml.cs, copy code for Add, Edit and Remove button handlers to ProductListView.xaml.cs.
    • Paste over the button click handlers for add, edit and remove.
    • Modify the code to create a ProductDetailViewModel and pass it to the ctor of a ProductDetailView, which you show modally
      • Make sure to set the Categories property on detailModel to viewModel.Categories
      • Notice how for edit mode we call BeginEdit, then either EndEdit or CancelEdit
        // Add item
        private void addItemButton_Click(object sender, RoutedEventArgs e)
        {
            // Create a product detail model
            Product newItem = new Product();
            ProductDetailViewModel detailModel = new ProductDetailViewModel(newItem);
    
            // Set categories
            detailModel.Categories = viewModel.Categories;
    
            // Show ProductDetail view
            ProductDetailView itemDetail = new ProductDetailView(detailModel);
            itemDetail.Closed += (s, ea) =>
            {
                if (itemDetail.DialogResult == true)
                {
                    viewModel.Add(newItem);
                }
            };
            itemDetail.Show();
        }
    
        // Edit item
        private void editItemButton_Click(object sender, RoutedEventArgs e)
        {
            // Exit if no product selected
            if (viewModel.SelectedProduct == null) return;
    
            // Create a product detail model
            ProductDetailViewModel detailModel =
                new ProductDetailViewModel(viewModel.SelectedProduct);
    
            // Set categories
            detailModel.Categories = viewModel.Categories;
    
            // Start editing
            detailModel.BeginEdit();
    
            // Show ProductDetail view
            ProductDetailView itemDetail = new ProductDetailView(detailModel);
            itemDetail.Closed += (s, ea) =>
            {
                if (itemDetail.DialogResult == true)
                {
                    // Confirm changes
                    detailModel.EndEdit();
                }
                else
                {
                    // Reject changes
                    detailModel.CancelEdit();
                }
            };
            itemDetail.Show();
        }
    
        // Remove item
        private void removeItemButton_Click(object sender, RoutedEventArgs e)
        {
            // Exit if no product selected
            if (viewModel.SelectedProduct == null)
            {
                return;
            }
    
            // Confirm remove, then remove
            if (MessageBox.Show("Are you sure?", "Remove Item", MessageBoxButton.OKCancel)
                == MessageBoxResult.OK)
            {
                viewModel.Remove();
            }
        }
    }
  22. Run the app and click the Edit button to increase the unit price of the selected product.
    • Click the Add button to add a new product
    • Click the Remove button to remove an existing product
    • Click the Reject Changes button to abandon pending changes
    • Repeat the editing, adding and removal
    • Click the Save Changes button to persist inserts, deleted and updates
      • Batch changes are propagated to the service in one trip and persisted to the database in a single transaction

    edit-prod

With the assistance of the Simple MVVM Toolkit, you have now built a real-world  Silverlight application based on the MVVM pattern that leverages WCF RIA Services for entity retrieval and persistence.  This includes a unit test project that relies on the injected ViewModel locator to use a mock service agent.  You have also used events for two-way communication between a View and its ViewModel.

Download the completed project here.

Last edited Apr 28, 2011 at 1:47 PM by tonysneed, version 7

Comments

junwebhead May 13, 2012 at 3:45 AM 
Hi Tony,

I never had a chance to thank you for your awesome work. Thank you very much!

To be honest, I've been struggling with MVVM for some time already until I found simplemvvmtoolkit.

Other examples/videos are just to vague for me. I mean they are too simple to be a realistic reference. Your RealWorld demonstration however is very helpful together with the great documentation and active discussions.
Thank you!

BravcM Jun 2, 2011 at 10:43 AM 
Very, very good! At least one tutorial which covers MVVM, Entity Framework and WCF RIA Services!
Perhaps in reflection how to separate Products View as dynamic view (module) which is loaded on demand from xap file (using MEF)?
Thank you very much!

blorincz May 6, 2011 at 3:09 AM 
This tutorial and project were immensely helpful. Thank you very much for your work.