Log in to like this post! Converting ASP.Net Multi-tier Applications to Applications with a Silverlight Client [Infragistics] Mihail Mateev / Saturday, July 10, 2010 This article describes how can convert an ASP.Net multi-tier business application to business application with Silverlight client. There are a lot of business n-tier applications, developed like ASP.Net applications. Sometimes it is an advantage if it is possible quickly and easily to convert the client side of these applications from ASP.Net to Silverlight. Silverlight proposes rich environment for development and it is very similar to WPF framework. Often Silverlight applications could be created quicker than most of ASP.Net based applications and easily ported to applications with WPF based desktop clients. Demo application presents simple multi-tier ASP.Net application with four business object classes : ContactPerson, Address, EmailAddress and PhoneNumber. We will add a Silverlight client application with WCF RIA Services link. Then we will add Domain Service Class and add a business logic there using the existing classes with business logic and business objects. Finally we will add UI components , related with the Domain Context from our Domain Service Class. Components of the ASP.Net n-tier applications: Generally, ASP.Net application has four components: Business Objects (BO) First, there are a number of business objects that live in the ASPNTierApp.BO namespace ASP.Net application, indicated by the rectangle on the right. The classes used for these objects in this sample are the ContactPerson, the Address, the EmailAddress and the PhoneNumber. They don't have any behavior, and can therefore be considered as "dumb" objects. All they can do, is hold and expose data through their public properties. Additionally, there is a generic list for each BO object like ContactPersonList, EmailAddressList and so on. Each of the other three components of the application has a reference to the objects in the Business Objects layer. This means that the web site can consume objects like ContactPerson that are returned from the business layer that in turn got them from the data access layer. Business Logic Layer (Bll) In the middle of the diagram, you see the Business Logic Layer; the bridge between the web site and the data access layer. The Bll gets instructions from the presentation layer (the web site in this example), to carry out tasks, like retrieving items from the data layer, or sending changed objects back into this layer. Additionally, it can perform tasks like enforcing security and carrying out validation, as you saw in part two of this article series. Data Access Layer (Dal) The data access layer contains the code that directly interacts with the data source, a SQL Server database in the case of the Contact Person Manager application but this could be any other kind of data source, like text or XML files, different databases and any other data source you can come up with. Presentation Layer At the top of the diagram, you see the Web Site, the presentation layer of this application. It's the web site and the pages and code it contains that is the main subject of this article, as you have already seen the other three parts in the previous two articles. Many people asked how can migrate the Presentation Layer in their multi-tier applications from ASP.Net to Silverlight, without changing the business logic and structure of the used business objects. One possible way is to use a Silverlight client application with the WCF RIA Services link. It is possible to realize the application with the Silverlight WCF Services or other kind of WEB Services, but with a WCF RIA Services we could write the business logic only one way for both: client and server side. Demo Application: In this article we will create a demo ASP.Net application that includes these layers. After that we will add a Silverlight application with the WCF RIA Services link. Requirements: Visual Studio 2010 Silverlight Tools for Visual Studio 2010 Silverlight 4 Toolkit (April) - optional You need also an installed SQL Server Express or higher license. Steps to create a sample ASP.Net multi-tired application: Create an ASP.Net Application Create a sample Demo ASP.Net application project: Create an ASP.Net WEB Application Create objects from Data Access Layer Create Business Objects Create class definitions from Business Logic Layer (BLL) Data Access Layer Application uses a sample database, named "Migration". You could download a backup of that database here. Connection string is saved in the Web.Config file. There is a simple class AppConfiguration, that returns a connection string. public class AppConfiguration { /// <summary> /// Returns the connectionstring for the application. /// </summary> public static string ConnectionString { get { return ConfigurationManager.ConnectionStrings["Migration"].ConnectionString; } } } <connectionStrings> <add name="Migration" connectionString="Data Source=(local)\SQLEXPRESS;Initial Catalog=SilverligtMigration;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings> There are four classes, providing data manipulation with database:ContactPersonDao, AddressDao, EmailAddressDao and PhoneNumberDao For each of these classes there are methods to select, edit, delete and update data from the database: For example in the ContactPersonDao class method GetList returns a list with all items from ContactPerson table. Method Save provides Insert/Update manipulation. These methods calls stored procedures from database. /// <summary> /// Returns a list with ContactPerson objects. /// </summary> /// <returns>A generics List with the ContactPerson objects.</returns> public static ContactPersonList GetList() { ContactPersonList tempList = null; using (SqlConnection myConnection = new SqlConnection(AppConfiguration.ConnectionString)) { SqlCommand myCommand = new SqlCommand("sprocContactPersonSelectList", myConnection); myCommand.CommandType = CommandType.StoredProcedure; myConnection.Open(); using (SqlDataReader myReader = myCommand.ExecuteReader()) { if (myReader.HasRows) { tempList = new ContactPersonList(); while (myReader.Read()) { tempList.Add(FillDataRecord(myReader)); } } myReader.Close(); } } return tempList; } /// <summary> /// Saves a contact person in the database. /// </summary> /// <param name="myContactPerson">The ContactPerson instance to save.</param> /// <returns>The new ID if the ContactPerson is new in the database or the existing ID when an item was updated.</returns> public static int Save(ContactPerson myContactPerson) { int result = 0; using (SqlConnection myConnection = new SqlConnection(AppConfiguration.ConnectionString)) { SqlCommand myCommand = new SqlCommand("sprocContactPersonInsertUpdateSingleItem", myConnection); myCommand.CommandType = CommandType.StoredProcedure; if (myContactPerson.Id == -1) { myCommand.Parameters.AddWithValue("@id", DBNull.Value); } else { myCommand.Parameters.AddWithValue("@id", myContactPerson.Id); } myCommand.Parameters.AddWithValue("@firstName", myContactPerson.FirstName); myCommand.Parameters.AddWithValue("@lastName", myContactPerson.LastName); if (String.IsNullOrEmpty(myContactPerson.MiddleName)) { myCommand.Parameters.AddWithValue("@middleName", DBNull.Value); } else { myCommand.Parameters.AddWithValue("@middleName", myContactPerson.MiddleName); } myCommand.Parameters.AddWithValue("@dateOfBirth", myContactPerson.DateOfBirth); myCommand.Parameters.AddWithValue("@contactpersonType", myContactPerson.Type); DbParameter returnValue; returnValue = myCommand.CreateParameter(); returnValue.Direction = ParameterDirection.ReturnValue; myCommand.Parameters.Add(returnValue); myConnection.Open(); myCommand.ExecuteNonQuery(); result = Convert.ToInt32(returnValue.Value); myConnection.Close(); } return result; } Business objects: There are four business objects, representing data (tables) in the database: ContactPerson, Address, EmailAddress and PhoneNumber. They don't have any behavior, and can therefore be considered as "dumb" objects. All they can do is hold and expose data through their public properties public class Address { #region Members private int _id = -1; private string _street = String.Empty; private string _houseNumber = String.Empty; private string _zipCode = String.Empty; private string _city = String.Empty; private string _country = String.Empty; private ContactType _type = ContactType.NotSet; private int _contactPersonId = -1; #endregion //Members #region Public Properties #region Id /// <summary> /// Gets or sets the unique ID of the address. /// </summary> [DataObjectField(true, true, false)] public int Id { get { return _id; } set { _id = value; } } #endregion //Id #region Street /// <summary> /// Gets or sets the Street of the address. /// </summary> public string Street { get { return _street; } set { _street = value; } } #endregion //Street #region HouseNumber /// <summary> /// Gets or sets the HouseNumber of the address. /// </summary> public string HouseNumber { get { return _houseNumber; } set { _houseNumber = value; } } #endregion .... #region Type /// <summary> /// Gets or sets the type of the address, like business or personal. /// </summary> public ContactType Type { get { return _type; } set { _type = value; } } #endregion //Type #region ContactPersonId /// <summary> /// Gets or sets the ID of the owner (a <see cref="ContactPerson"/>) of the address. /// </summary> public int ContactPersonId { get { return _contactPersonId; } set { _contactPersonId = value; } } #endregion //ContactPersonId #endregion } Business Logic Layer: Business Logic Layer objects are the bridge between the web site and the data access layer. The Bll gets instructions from the presentation layer (the web site in this example), to carry out tasks, like retrieving items from the data layer, or sending changed objects back into this layer. There are four classes in that layer: AddressManager, ContactPersonManager, EmailAddressManager, PhoneNumberManager /// <summary> /// The AddressManager class is responsible for managing BO.Address objects in the system. /// </summary> [DataObject()] public class AddressManager { #region Public Methods #region Delete /// <summary> /// Deletes an address from the database. /// </summary> /// <param name="myAddress">The Address instance to delete.</param> /// <returns>Returns true when the object was deleted successfully, or false otherwise.</returns> [DataObjectMethod(DataObjectMethodType.Delete, true)] public static bool Delete(Address myAddress) { return AddressDao.Delete(myAddress.Id); } #endregion //Delete #region GetList /// <summary> /// Gets a list with Address objects for the requested contact person. /// </summary> /// <param name="contactPersonId">The ID of the ContactPerson for whom the addresses should be returned.</param> /// <returns>A list with address objects when the database contains addresses for the contact person, or an empty list otherwise.</returns> [DataObjectMethod(DataObjectMethodType.Select, true)] public static AddressList GetList(int contactPersonId) { return AddressDao.GetList(contactPersonId); } #endregion //GetList #region GetItem /// <summary> /// Gets a single Address from the database by its ID. /// </summary> /// <param name="id">The unique ID of the address in the database.</param> /// <returns>An Address object when the ID exists in the database, or <see langword="null"/> otherwise.</returns> [DataObjectMethod(DataObjectMethodType.Select, false)] public static Address GetItem(int id) { return AddressDao.GetItem(id); } #endregion //GetItem #region Save /// <summary> /// Saves an address in the database. /// </summary> /// <param name="myAddress">The Address instance to save.</param> /// <returns>The new ID if the Address is new in the database or the existing ID when an item was updated.</returns> [DataObjectMethod(DataObjectMethodType.Update | DataObjectMethodType.Insert, true)] public static int Save(Address myAddress) { myAddress.Id = AddressDao.Save(myAddress); return myAddress.Id; } #endregion //Save #endregion } Displaying a data from the Business objects: Displaying all Contact Persons in the System Because much of the work has already been done by writing the code in the BO, Bll and Dal layers, displaying a list of contact persons is now super easy. To display a list of users in a GridView, follow these steps: 1. Create a new page and switch to Design View. Add a GridView to the page by dragging it from the Toolbox on the design surface. 2. Open the GridView's Smart Task panel and under Choose Data Source select <New data source>. 3. In the Data Source Configuration Wizard, Select Object and click OK. 4. In the Choose a Business Object, make sure Show only data components is checked and then choose the appropriate business object from the list. In this case, I chose: ASPNTierApp.BLL.ContactPersonManager: 5. Next, Define Data Methods for the Data Source . 6. Finally, click Finish to close the wizard. Managing Contact Persons in the System ContactPersonPage.aspx page. ASP.NET offers many features that make it easy to work with data in a web page. You have the various DataSource controls to manage data, the GridView to display lists of records and the DetailsView and FormView controls to display a single record at the time, with insert and update capabilities. <asp:GridView ID="gvContactPersons" runat="server" AutoGenerateColumns="False" DataSourceID="odsContactPersons" DataKeyNames="Id" OnRowCommand="GvContactPersons_RowCommand" AllowPaging="True" CellPadding="4" GridLines="None"> <AlternatingRowStyle BackColor="#FFFFCC" /> <Columns> <asp:BoundField DataField="Id" HeaderText="Id" SortExpression="Id" /> <asp:BoundField DataField="FullName" HeaderText="Full Name" ReadOnly="True" SortExpression="FullName" /> <asp:BoundField DataField="DateOfBirth" HeaderText="Date of Birth" SortExpression="DateOfBirth" DataFormatString="{0:d}" HtmlEncode="False" /> <asp:BoundField DataField="Type" HeaderText="Type" /> <asp:ButtonField CommandName="Addresses" Text="Addresses" /> <asp:ButtonField CommandName="Edit" Text="Edit" /> <asp:TemplateField ShowHeader="False"> <ItemTemplate> <asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="False" CommandName="Delete" Text="Delete" OnClientClick="return confirm('Are you sure you want to delete this contact person?');"></asp:LinkButton> </ItemTemplate> </asp:TemplateField> </Columns> <HeaderStyle BackColor="#3366CC" ForeColor="White" /> <RowStyle BackColor="#CCFFFF" /> </asp:GridView> The AddEditContactPerson.aspx Page The markup of this page is really simple. It contains a standard HTML <table> containing standard rows and cells that in turn contain a number of ASP.NET controls, like TextBox, Calendar, Button and a few validator controls. You could get the full code from the demo application. Run ASP.Net N-Tier Application: Start window Add a new contact person form. Show addresses for a specified contact person. Convert the Client Side to a Silverlight Client Application Maybe the easiest way to convert the client side of an ASP.Net n-tier application to Silverlight client is to use a Silverlight client application with the WCF RIA Services link. WCF RIA Services propose a Domain Service Class. It can works integrated with ADO.Net Entity Framework, but at the same time proposes possibility customers to use own business objects. These objects must be compatible with the Silverlight implementation of the .Net Framework. Requirements: You need to install trial version of NetAdvantage for Silverlight Line of Business 10.2 . Demo sample Add a Silverlight Application, named SilverlightMigrationClient. Check "Enable WCF RIA Services" checkbox. Automatically SilverlightMigrationClientTestPage.aspx page will be set as start page. Add s Domain Service Class named MigrationDomainService to our ASP.Net Application. Business Objects Updates: To be possible to work with the existing business objects we need to add Key attribute for the key fields. Properties that can be changed in the Silverlight client and updated via WFC RIA Services needs also have Editable attribute public class ContactPerson { …. #region Id /// <summary> /// Gets or sets the unique ID of the contact person. /// </summary> [DataObjectField(true, true, false)] [Key] [Editable(true)] public int Id { get { return _id; } set { _id = value; } } #endregion //Id #region FirstName /// <summary> /// Gets or sets the first name of the contact person. /// </summary> [Editable(true)] public string FirstName { get { return _firstName; } set { _firstName = value; } } #endregion //FirstName …. } Implement methods containing application logic In the existing Domain Service Class you need to add methods to Select, Insert, Update and Delete operations: For operations Insert, Update and Delete implemented methods needs the same attribute: Insert, Update or Delete: [EnableClientAccess()] public class MigrationDomainService : DomainService { #region ContactPerson public ContactPersonList GetContactPersons() { return ContactPersonManager.GetList(); } public ContactPerson GetContactPersonItem(int id) { return ContactPersonManager.GetItem(id); } [Delete] public void DeleteContactPerson(ContactPerson person) { ContactPersonManager.Delete(person); } [Insert] public void InsertContactPerson(ContactPerson person) { ContactPersonManager.Save(person); } [Update] public void UpdateContactPerson(ContactPerson person) { ContactPersonManager.Save(person); } #endregion //ContactPerson #region Address public AddressList GetAddresses(int contactPersonId) { return AddressManager.GetList(contactPersonId); } public Address GetAddressItem(int id) { return AddressManager.GetItem(id); } [Delete] public void DeleteAddress(Address address) { AddressManager.Delete(address); } [Insert] public void InsertAddress(Address address) { AddressManager.Save(address); } [Update] public void UpdateAddress(Address address) { AddressManager.Save(address); } #endregion //Address ..... } Add UI Components: It is very easy to add a data source from the DomainContext of the your Domain Service Class. It is possible to add UI Component (XamGrid, DataForm etc..) associated with a specific business object. In the sample the Silverlight client application contains two XamGrid instances, containing data for all ContactPerson instances and Addresses for the selected ContactPerson. DomainDataSource class specifies the query, used to get the data. <riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my:ContactPerson, CreateList=true}" Height="0" LoadedData="ContactPersonDomainDataSourceLoadedData" Name="contactPersonDomainDataSource" QueryName="GetContactPersonsQuery" Width="0"> <riaControls:DomainDataSource.DomainContext> <my1:MigrationDomainContext /> </riaControls:DomainDataSource.DomainContext> </riaControls:DomainDataSource> A XamGrid component is used in the same way like with other kinds of data source. <ig:XamGrid HorizontalAlignment="Left" AutoGenerateColumns="False" ItemsSource="{Binding ElementName=contactPersonDomainDataSource, Path=Data}" Margin="10" Name="contactPersonXamGrid" VerticalAlignment="Top"> <ig:XamGrid.SelectionSettings> <ig:SelectionSettings RowSelection="Single" CellSelection="None" CellClickAction="SelectRow"/> </ig:XamGrid.SelectionSettings> <ig:XamGrid.Columns> <ig:TextColumn Key="Id"/> <ig:DateColumn Key="DateOfBirth" HeaderText="Date of Birth"/> <ig:TextColumn Key="FirstName"/> <ig:TextColumn Key="MiddleName"/> <ig:TextColumn Key="LastName"/> <ig:TextColumn Key="Type"/> <ig:UnboundColumn Key="Delete"> <ig:UnboundColumn.ItemTemplate> <DataTemplate> <Button Content="Delete"> <ig:Commanding.Command> <ig:XamGridRowCommandSource CommandType="Delete" EventName="Click"></ig:XamGridRowCommandSource> </ig:Commanding.Command> </Button> </DataTemplate> </ig:UnboundColumn.ItemTemplate> </ig:UnboundColumn> <ig:UnboundColumn Key="Edit"> <ig:UnboundColumn.ItemTemplate> <DataTemplate> <Button Content="Edit"> <ig:Commanding.Command> <ig:XamGridRowCommandSource CommandType="Edit" EventName="Click"></ig:XamGridRowCommandSource> </ig:Commanding.Command> </Button> </DataTemplate> </ig:UnboundColumn.ItemTemplate> </ig:UnboundColumn> <ig:UnboundColumn Key="Addresses"> <ig:UnboundColumn.ItemTemplate> <DataTemplate> <Button Content="Addresses" Click="Button_Click"> </Button> </DataTemplate> </ig:UnboundColumn.ItemTemplate> </ig:UnboundColumn> </ig:XamGrid.Columns> </ig:XamGrid> To display data in the XamGrid with addresses you need to add some code: private void Button_Click(object sender, RoutedEventArgs e) { // GetSelectedId method that returns the id of the selected ContactPerson object int id = GetSelectedId(this.contactPersonXamGrid); UpdateAddressGrid(id); } private void UpdateAddressGrid(int id) { this.CurrentCustomerId = id; addressDomainDataSource.Load(); } There is added additional dependency property CurrentCustomerId used when load data for addresses: public static readonly DependencyProperty CurrentCustomerIdProperty = DependencyProperty.Register("CurrentCustomerId", typeof(int), typeof(MainPage), null); public int CurrentCustomerId { get { return (int)this.GetValue(CurrentCustomerIdProperty); } set { this.SetValue(CurrentCustomerIdProperty, value); } } DomainDataSource, used for Addresses uses Query that takes parameter from dependency property CurrentCustomerId. <riaControls:DomainDataSource AutoLoad="False" d:DesignData="{d:DesignInstance my:Address, CreateList=true}" Height="0" LoadedData="AddressDomainDataSourceLoadedData" Name="addressDomainDataSource" QueryName="GetAddressesQuery" Width="0"> <riaControls:DomainDataSource.DomainContext> <my1:MigrationDomainContext /> </riaControls:DomainDataSource.DomainContext> <riaControls:DomainDataSource.QueryParameters> <riaControls:Parameter ParameterName="contactPersonId" Value="{Binding ElementName=MainWindow, Path=CurrentCustomerId}" /> </riaControls:DomainDataSource.QueryParameters> </riaControls:DomainDataSource> <ig:XamGrid Grid.Row="0" HorizontalAlignment="Left" ItemsSource="{Binding ElementName=addressDomainDataSource, Path=Data}" Margin="10" Name="addressXamGrid" VerticalAlignment="Top"> <ig:XamGrid.SelectionSettings> <ig:SelectionSettings RowSelection="Single" CellSelection="None" CellClickAction="SelectRow"/> </ig:XamGrid.SelectionSettings> <ig:XamGrid.Columns> <ig:TextColumn Key="Id"/> <ig:TextColumn Key="Country"/> <ig:TextColumn Key="City"/> <ig:TextColumn Key="Street"/> <ig:TextColumn Key="HouseNumber" HeaderText="House Number"/> <ig:TextColumn Key="ZipCode" HeaderText="Zip Code"/> <ig:TextColumn Key="ContactPersonId" HeaderText="Contact Person Id"/> <ig:TextColumn Key="Type"/> <ig:UnboundColumn Key="Delete"> <ig:UnboundColumn.ItemTemplate> <DataTemplate> <Button Content="Delete" Click="DeleteButton_Click"> </Button> </DataTemplate> </ig:UnboundColumn.ItemTemplate> </ig:UnboundColumn> <ig:UnboundColumn Key="Edit"> <ig:UnboundColumn.ItemTemplate> <DataTemplate> <Button Content="Edit"> <ig:Commanding.Command> <ig:XamGridRowCommandSource CommandType="Edit" EventName="Click"></ig:XamGridRowCommandSource> </ig:Commanding.Command> </Button> </DataTemplate> </ig:UnboundColumn.ItemTemplate> </ig:UnboundColumn> </ig:XamGrid.Columns> </ig:XamGrid> To submit changes in the databases there is used a button. In its event there is called DomainDataSource.SubmitChanges() method private void BtnAddressUpdate_Click(object sender, RoutedEventArgs e) { addressDomainDataSource.SubmitChanges(); } Add new item (ContactPerson): <Grid HorizontalAlignment="Left" Margin="10,25,10,25" Name="grid1" VerticalAlignment="Top"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> … </Grid.RowDefinitions> <sdk:Label Content="Date Of Birth:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" /> <sdk:DatePicker Grid.Column="1" Grid.Row="0" Height="23" HorizontalAlignment="Left" Margin="3" Name="dateOfBirthDatePicker" SelectedDate="{Binding Path=DateOfBirth, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}" VerticalAlignment="Center" Width="120" /> … <Button Grid.Column="1" x:Name="BtnContactInsert" Grid.Row="7" Height="23" HorizontalAlignment="Left" Margin="3" Content="Insert" VerticalAlignment="Center" Width="120" Click="BtnContactInsert_Click" /> </Grid> To add a new ContactPerson object firstly you need to create an instance of ContactPerson class and set is as DataContext of grid1 - the panel where are placed controls, bound to ContactPerson properties. private void BtnContactAdd_Click(object sender, RoutedEventArgs e) { _contactPerson = new ContactPerson { Id = -1, DateOfBirth = DateTime.Now }; DetailsContainer.IsEnabled = true; grid1.DataContext = _contactPerson; } When want to add a new instance of ContactPerson to database, you need to add it to collection ContactPersons in the DomainContext, submit the changes and load the data from the data source again: private void BtnContactInsert_Click(object sender, RoutedEventArgs e) { if (this._contactPerson != null) { MigrationDomainContext context = contactPersonDomainDataSource.DomainContext as MigrationDomainContext; if (context != null) { contactPersonDomainDataSource.SubmittedChanges -= ContactPersonDomainDataSourceSubmittedChanges; context.ContactPersons.Add(_contactPerson); contactPersonDomainDataSource.SubmitChanges(); contactPersonDomainDataSource.SubmittedChanges += new EventHandler<SubmittedChangesEventArgs>(ContactPersonDomainDataSourceSubmittedChanges); } } } void ContactPersonDomainDataSourceSubmittedChanges(object sender, SubmittedChangesEventArgs e) { contactPersonDomainDataSource.Load(); this._contactPerson = null; DetailsContainer.IsEnabled = false; } Run the application Start Screen. Add a new ContactPerson instance. Show addresses for specified customer. This scenario presents one possible and easy approach to change a client part of your ASP.Net n-tier application with a Silverlight client. It is very similar to use WPF client with the same application. A sample demonstrate a flexibility of multi-tired application to change a client side with less efforts.