Monday, May 2, 2011

Silverlight 4.0 Using MVVM

How To: Implement MVVM (Model-View-ViewModel) in Silverlight 4.0

The MVVM is a design pattern, typically used in Silverlight and WPF application development. The Model-View-ViewModel (MVVM) pattern helps you to cleanly separate the business and presentation logic of your application from its user interface (UI). The Model-View-ViewModel (MVVM) pattern provides a flexible way to build Silverlight applications that promotes code re-use, simplifies maintenance and supports testing.

Using the MVVM pattern, the UI of the application and the underlying presentation and business logic is separated into three separate classes: the View, which encapsulates the UI and UI logic; the View Model, which encapsulates presentation logic and state; and the Model, which encapsulates the application's business logic and data.

An example of the how the Model, View and ViewModel components relate to each other is shown in the following diagram:

Picture1

In the MVVM pattern, the View encapsulates the UI and any UI logic, the ViewModel encapsulates presentation logic and state, and the Model encapsulates business logic and data. The View interacts with the ViewModel through Data Binding, Commands, and Change Notification events. The ViewModel Queries, Observes, and Coordinates updates with the Model. It also converts and validates data as necessary for display in the View.

The following figure explains the same:Picture1

The Model: – The Model in MVVM represents typically Business Data and Business Logic. The BL takes care of retrieval and management of application data. It also takes care of data validations and data consistency required for the business application. Typically, the model represents the client-side domain model for the application. Model should not contain any client specific behavior or any application specific logic. Typically it contains ADO.NET Entity Framework, WCF Data Service or WCF RIA Service.

Model supports, property and collection change notification with the help of INotifyPropertyChanged and INotifyCollectionChanged interfaces. Collection objects are generally derived from ObservableCollection<T>, which in turn has INotifyCollectionChanged interface implemented. To support data validation one can use IDataErrorInfo or INotifyDataErrorInfo interface by creating partial class/s for business model.

These interfaces allow Silverlight data binding to be notified when values change so that the UI can be updated. They support data validation and error reporting in the UI layer.

The View: – Normally, View is used to present Model what user is looking for. Typically this is done with the help of XAML UI Elements i.e. User Controls or Custom Controls in Silverlight. The code-behind for this XAML page contains a constructor calling InitializeComponent method. Optionally, it could contain code for some complex animation or if XAML is not sufficient to express some UI Logic.

The ViewModel: - The ViewModel in the MVVM pattern encapsulates the presentation logic and data for the view. VM has no direct knowledge of View or View’s specific implementation or Type. The responsibility of VM is to provide all required Properties and Commands, to which View is interested in. Following diagram explains what one can have in ViewModel.

Picture1

Let’s get into the step-by-step implementation of MVVM in Silverlight 4.0 application. For this implementation I have used the following architecture.

Picture1


Building a Silverlight application using MVVM

You can download complete source code for this exercise Here.

1. Create a blank solution for .NET Framework 4.0 in Visual Studio 2010. Name it SilverlightUsingMVVM. In this solution we will create following projects to implement MVVM in Silverlight application.

  • ·SilverlightUsingMVVM.Model – This is a simple class library, which will hold our Business Data and Business Logic. We will use Entity Framework to create Business Data. You may add few partial classes to provide data validation.
  • SilverlightUsingMVVM.Service – This is an IIS hosted WCF service class library, which will provide a communication channel between our Business Data Model and the Silverlight ViewModel layer.
  • SilverlightUsingMVVM.View – This is a Silverlight application project with its Web hosting project named SilverlightUsingMVVM.View.Web.

Note: - This exercise assumes that you are using SQL Server or SQL Express 2008 with TrainingDB database in it. Also you have Products table with couple of rows of data. If not, please create a database named TrainingDB in your SQL Server / Express and then run the following script to create the required Products table under it.

USE [TrainingDB]
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Products]') AND type in (N'U'))
DROP TABLE [dbo].[Products]
GO

USE [TrainingDB]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[Products](
[ID] [int] NOT NULL,
[Name] [varchar](50) NOT NULL,
[Description] [varchar](200) NULL,
[Price] [money] NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO





2. Right-click on the solution Add > New Prodjct > Visual C# > Windows > Class Library. Name it SilverlightUsingMVVM.Model. Delete Class1.cs file from the project.



3. Right-click on SilverlightUsingMVVM.Model project Add > New Item > Data > ADO.NET Entity Data Model, name it TrainingDB.edmx and click Add. Make sure Generate from Database option is selected and click Next. Click New Connection, put .\SQLEXPRESS as server name and select TrainingDB as database name, click Test Connection. If Text Connection succeeded click Ok. Click Next, expand Tables node and select Products(dbo) table, click Finish to add the Entity Framework Data Model for our Business Data.



4. Close the .edmx diagram.



5. Save all your work and build the solution.



6. Now, right-click on the solution Add > New Project > WCF > WCF Service Application, name it SilverlightUsingMVVM.Service click Add.



7. Right-click on IService1.cs and rename it to IProductService.cs, click Yes in the message box.



8. Right-click on Service1.svc and rename it to ProductService.svc.



9. Open ProductService.cs class right-click on Service1 class name select Refactor > Rename, change name to ProductService, click Ok. Click Apply in the next window.



10. Right-click on References node Add Reference, select .NET tab and select System.Data.Entity.dll and click Add. Again right-click on References node Add Reference click Projects and select SilverlightUsingMVVM.Model and click Ok.



11. Clean the class by deleting everything in it as follows:




namespace SilverlightUsingMVVM.Service
{
public class ProductService : IProductService
{
}
}





12. Open IProductService.cs file. Delete everything in it as follows:




namespace SilverlightUsingMVVM.Service
{
[ServiceContract]
public interface IProductService
{
}
}





13. Right-click on SilverlightUsingMVVM.Service project, Add > New Item > Visual C# > XML File, name it clientaccesspolicy.xml and click Add. We are adding this file because our WCF service in not a part of Silverlight application web project.



14. Copy the following xml code to this file.




<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*"/>"
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>





15. Open IProductService.cs file. Add following using statement to this interface.



using SilverlightUsingMVVM.Model;



16. Add the following method with OperationContract attribute to this interface.




using System.Collections.Generic;
using System.ServiceModel;
using SilverlightUsingMVVM.Model;

namespace SilverlightUsingMVVM.Service
{
[ServiceContract]
public interface IProductService
{
[OperationContract]
List<Product> GetAllProducts();

[OperationContract]
bool InsertProduct(Product product);
}
}





17. Switch back to ProductService.cs file and implement IProductService interface to this class as follows:




using System;
using System.Collections.Generic;
using System.Linq;
using SilverlightUsingMVVM.Model;
using System.ServiceModel.Activation;

namespace SilverlightUsingMVVM.Service
{
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
public class ProductService : IProductService
{
TrainingDBEntities db;

public ProductService()
{
db = new TrainingDBEntities();
}

public List<Product> GetAllProducts()
{
return db.Products.ToList();
}

public bool InsertProduct(Product product)
{
try
{
db.Products.AddObject(product);
db.SaveChanges();
return true;

}
catch (Exception e)
{
return false;
}
}
}
}





18. Open App.config file from SilverlightUsingMVVM.Model project and copy following lines:



 





<connectionStrings>
<add name="TrainingDBEntities" connectionString="metadata=res://*/TrainingDB.csdl|res://*/TrainingDB.ssdl|res://*/TrainingDB.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=.\sqlexpress;initial catalog=TrainingDB;integrated security=True;pooling=False;multipleactiveresultsets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>




19. Now open Web.config file from SilverlightUsingMVVM.Service project and paste it as follows:




<configuration>
<connectionStrings>
<add name="TrainingDBEntities" connectionString="metadata=res://*/TrainingDB.csdl|res://*/TrainingDB.ssdl|res://*/TrainingDB.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=.\sqlexpress;initial catalog=TrainingDB;integrated security=True;pooling=False;multipleactiveresultsets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>
<system.web>





Note: If you will miss this step, after completion of the entire solution you will get Remote Server Error: Not Found.



20. Close all .config files that are opened.



21. Build the solution one more time to check that things are placed correctly.



22. Back to solution explorer, right-click Add > New Project > Silverlight > Silverlight Application, name it SilverlightUsingMVVM.Viewer and click Ok.



23. After project gets created close MainPage.xaml as this is not the right time to work with it.



24. Right-click on References node of SilverlightUsingMVVM.Viewer project and click Add Service Reference. Click Discover button. If you get http://localhost:<protnumber>/ProductService.svc in Address bar, click Go to host and find the service. If you get 1 service(s) found at address …, change the namespace to ProductServiceReference and click Ok. This will add all the required proxy classes of the service that you have.



25. Now we will create a service wrapper which will hold the service proxy and provide communication with the service. To do this add a folder named ServiceWrapper to SilverlightUsingMVVM.Viewer project by right-clicking on this project Add > New Folder.



26. Add an interface named IServiceWarpper to this folder. Right-click on the folder Add > New Item > Code > Class, name it IServiceWrapper.cs and click Add.



27. Change the word class to interface. Your code should look like:





   1: namespace SilverlightUsingMVVM.Viewer.ServiceWrapper



   2: {



   3:     public interface IServiceWrapper



   4:     {



   5:  



   6:     }



   7: }




28. Adding this interface will provide more loosely coupled architecture for our application.



29. Add the following code to this interface.




using System;
using SilverlightUsingMVVM.Viewer.ProductServiceReference;

namespace SilverlightUsingMVVM.Viewer.ServiceWrapper
{
public interface IServiceWrapper
{
void GetAllProducts(EventHandler<GetAllProductsCompletedEventArgs> callbackHandler);
void InsertProduct(Product product, EventHandler<InsertProductCompletedEventArgs> callbackHandler);
}
}





30. These are two methods our ViewModel will use to make a call to our WCF service.



31. Right-click on the same ServiceWrapper folder Add > Class, name it ServiceWarpper.cs and click Add.



32. Implement IServiceWrapper interface to this class and add the following code to it.




using System;
using SilverlightUsingMVVM.Viewer.ProductServiceReference;

namespace SilverlightUsingMVVM.Viewer.ServiceWrapper
{
public class ServiceWarpper : IServiceWrapper
{
ProductServiceClient proxy;
public ServiceWarpper()
{
proxy = new ProductServiceClient();
}

public void GetAllProducts(EventHandler<GetAllProductsCompletedEventArgs> callbackHandler)
{
proxy.GetAllProductsCompleted += callbackHandler;
proxy.GetAllProductsAsync();
}

public void InsertProduct(Product product, EventHandler<InsertProductCompletedEventArgs> callbackHandler)
{
proxy.InsertProductCompleted += callbackHandler;
proxy.InsertProductAsync(product);
}
}
}





33. Next is, let’s create a coming class whose object will perform a given specific Action.



34. Right-click on SilverlightUsingMVVM.Viewer project Add > New Folder, name it Commanding.



35. We will create a RelayCommand class to perform this Action. Right-click on this folder Add > Class, name it RelayCommand.cs and click Add.



36. Implement ICommand interface to this class and add the following code to the class.




using System;
using System.Windows.Input;

namespace SilverlightUsingMVVM.Viewer.Commanding
{
public class RelayCommand : ICommand
{
private readonly Action actionHandler;
private bool isActionable;

public RelayCommand(Action action)
{
actionHandler = action;
}

public bool IsActionable
{
get
{
return isActionable;
}
set
{
if (value != isActionable)
{
isActionable = value;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
}

public bool CanExecute(object parameter)
{
//return IsActionable;
return true;
}

public event EventHandler CanExecuteChanged;

public void Execute(object parameter)
{
actionHandler();
}
}
}





37. Ok, now let’s put our focus on defining ViewModel layer of this architecture.



38. Right-click on SilverlightUsingMVVM.Viewer project Add > New Folder, name it ViewModel.



39. Right-click on this folder Add > Class, name it ViewModelBase.cs. This class will provide property change notification.



40. Implement INotifyPropertyChanged interface to this class and add the following code to it.




using System.ComponentModel;


namespace SilverlightUsingMVVM.Viewer.ViewModel
{
public class ViewModelBase : INotifyPropertyChanged
{
public bool IsDesignTime
{
get
{
return DesignerProperties.IsInDesignTool;
}
}

public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}





41. Right-click on ViewModel folder Add > Class, name it ProductsViewModel.cs.



42. Inherit this class from ViewModelBase class created in the previous step and add the following code to it.




using System.Collections.ObjectModel;
using SilverlightUsingMVVM.Viewer.ProductServiceReference;
using SilverlightUsingMVVM.Viewer.Commanding;
using SilverlightUsingMVVM.Viewer.ServiceWrapper;

namespace SilverlightUsingMVVM.Viewer.ViewModel
{
public class ProductsViewModel : ViewModelBase
{
public ProductsViewModel()
: this(new ServiceWarpper())
{

}
public ProductsViewModel(IServiceWrapper wrapper)
{
Product prd = new Product();
CurrentProduct = prd;
if (!IsDesignTime)
{
if (wrapper != null)
{
psWrapper = wrapper;
GetAllCustomers();
WireCommands();
}
}
}

#region Properties
private IServiceWrapper psWrapper;
private ObservableCollection<Product> products;
public ObservableCollection<Product> Products
{
get
{
return products;
}
set
{
if (products != value)
{
products = value;
OnPropertyChanged("Products");
}
}
}
private Product currentProduct;
public Product CurrentProduct
{
get
{
return currentProduct;
}
set
{
if (currentProduct != value)
{
currentProduct = value;
OnPropertyChanged("CurrentProduct");
}
}
}
#endregion

#region Commands
public RelayCommand InsertProductCommand
{
get;
private set;
}
#endregion

private void GetAllCustomers()
{
psWrapper.GetAllProducts((sender, args) => Products = args.Result);
}
private bool status;
private void InsertProduct()
{
psWrapper.InsertProduct(CurrentProduct, (sender, args) => status = args.Result);
Products.Add(CurrentProduct);
}

private void WireCommands()
{
InsertProductCommand = new RelayCommand(InsertProduct);
}

}
}





43. This class is a key class in our MVVM architecture, as it exposes all the required Properties and Commands to our View.



44. Now it’s time to create a View for Silverlight Application. In our case MainPage.xaml will act as a View for Products. So open MainPage.xaml.



45. Add the following code to this MainPage.xaml, observe highlighted part of the code.




<UserControl x:Class="SilverlightUsingMVVM.Viewer.MainPage"
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:local="clr-namespace:SilverlightUsingMVVM.Viewer.ViewModel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
<UserControl.Resources>
<local:ProductsViewModel x:Key="vmProducts"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" DataContext="{Binding Source={StaticResource vmProducts}}">
<sdk:TabControl Height="278" HorizontalAlignment="Left" Margin="10,10,0,0" Name="tabControl1" VerticalAlignment="Top" Width="378">
<sdk:TabItem Header="Product List" Name="tabItem1">
<Grid>
<sdk:DataGrid Grid.Row="0" ItemsSource="{Binding Products, Mode=TwoWay}" AutoGenerateColumns="False" HorizontalAlignment="Left" Margin="10,10,0,0" Name="dataGrid1" VerticalAlignment="Top">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Product Id" Binding="{Binding ID, Mode=TwoWay}"/>
<sdk:DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=TwoWay}"/>
<sdk:DataGridTextColumn Header="Description" Binding="{Binding Description, Mode=TwoWay}"/>
<sdk:DataGridTextColumn Header="Price" Binding="{Binding Price, Mode=TwoWay}"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
</Grid>
</sdk:TabItem>
<sdk:TabItem Header="Add New Product" Name="tabItem2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Product ID: " HorizontalAlignment="Right"/>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="txtProductID" Width="200" HorizontalAlignment="Left" Text="{Binding Path=CurrentProduct.ID, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Product Name: " HorizontalAlignment="Right"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="txtProductName" Width="200" HorizontalAlignment="Left" Text="{Binding Path=CurrentProduct.Name, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Description: " HorizontalAlignment="Right"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtProductDescription" Width="200" HorizontalAlignment="Left" Text="{Binding Path=CurrentProduct.Description, Mode=TwoWay}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Product Price: " HorizontalAlignment="Right"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="txtProductPrice" Width="200" HorizontalAlignment="Left" Text="{Binding Path=CurrentProduct.Price, Mode=TwoWay}"/>
<Button Grid.Row="4" Grid.Column="1" Content="Save Product" Width="200" HorizontalAlignment="Left" Command="{Binding InsertProductCommand}"/>
</Grid>
</sdk:TabItem>
</sdk:TabControl>
</Grid>
</UserControl>





46. That’s it. Run your Silverlight application by pressing F5, watch the values in DataGrid on the Tab1 Product Details, switch to Tab2 Add New Product, fill the form and click Save Product button. Again switch back to Tab1 Product Details and observer last entry in the DataGrid. You have your newly added product listed in the grid.

No comments:

Post a Comment