Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 15: ASP.NET MVC

Continuing in our discussion of Silverlight 3 and the update to .NET RIA Services.  I have been updating  the example from my Mix09 talk “building business applications with Silverlight 3”.  I customer recently asked about using ASP.NET MVC and Silverlight with RIA Services.  There specific scenario was an application with the complex admin interface in Silverlight but using ASP.NET MVC for the consumer facing part of the web to get the maximum reach.  The customer wanted to share as much of their application logic as possible. 

So, to address this, i thought I’d update my Mix 09 demo to have an ASP.NET MVC view as well as a silverlight view. 

You can watch the original  video of the full session

You can see the full series here

The demo requires (all 100% free and always free):

  1. VS2008 SP1 (Which includes Sql Express 2008)
  2. Silverlight 3 RTM
  3. .NET RIA Services July '09 Preview
  4. ASP.NET MVC 1.0

Also, download the full demo files and check out the running application.

The architecture we are going to look at today us focused on building the ASP.NET MVC head on the RIA Servces based app logic.  As you will see this is easy to do and shares all the great UI validation support.

image

To start with I took the original application and deleted the MyApp.Web project and added a new project that is ASP.NET MVC based.  You could have just as easily added another web project to the same solution. 

image

Then I associated the Silverlight application with this web project.  Be sure to turn on the RIA Services link as well.  This is what controls the codegen to the Silverlight client.

image

Then i copied over the Authentication related DomainServices from the web project.  

Then i added my northwind.mdf file to App_Data, built my Entity Framework model and finally added my domain service and updated it exactly the way did in Part 2.  The one tweak we need to do is make each of the methods in the DomainService virtual.. this has no real effect on the silverlight client, but it allows up to build a proxy for the MVC client. 

 [EnableClientAccess]
 public class SuperEmployeeDomainService : LinqToEntitiesDomainService<NORTHWNDEntities>
 {
  
     public virtual IQueryable<SuperEmployee> GetSuperEmployees()
     {
         return this.Context.SuperEmployeeSet;
     }
  
     public override void Submit(ChangeSet changeSet)
     {
         base.Submit(changeSet);
     }
  
     public virtual void UpdateSuperEmployee(SuperEmployee currentSuperEmployee)
     {
         this.Context.AttachAsModified(currentSuperEmployee, 
                                       ChangeSet.GetOriginal(currentSuperEmployee));            
     }

Running the MyAppTestPage.aspx should show that we have the same Silverlight app up and running.. 

image

We can now focus on the ASP.NET MVC part of this.

In the Controllers direction, open up HomeContollers.cs.

    1: [HandleError]
    2: public class HomeController : Controller
    3: {
    4:    SuperEmployeeDomainService domainService = 
    5:        DomainServiceProxy.Create<SuperEmployeeDomainService>();

Line 5 calls a factory method that creates a DomainService wrapper.. this allows you to have a clean, direct calling syntax for the DomainService, but allows the system to run through it’s standard pipeline of validation, authorization, etc.    Note, with the current CTP, you will need to reference DomainServiceExtensions.dll from this sample to get this functionality. 

    1: public ActionResult Index()
    2:  {
    3:      return View("Index", domainService.GetSuperEmployees());
    4:  }

Then index is very easy.. we simply pass the results of calling GetSuperEmployee() as our model.  This allows us to share any business logic that filters the results… I can write it once and share it between my Silverlight and ASP.NET MVC app. 

Nothing at all remarkable about the view..  Index.aspx in the \Views directory.

    1: <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
    2:  Inherits="System.Web.Mvc.ViewPage<IQueryable<MyApp.Web.Models.SuperEmployee>>" %>
    3:  
    4: <asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
    5:     Home Page
    6: </asp:Content>
    7:  
    8: <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    9:    
   10: <h2>List of Employees</h2> 
   11: <ul> 
   12: <% foreach (var emp in Model) { %> 
   13:  
   14: <li>                  
   15:     <%=Html.ActionLink(emp.Name, "Details", new { id = emp.EmployeeID })%>          
   16:     Origin: 
   17:     <%=Html.Encode(emp.Origin)%>    
   18: </li> 
   19: <% } %> 
   20:     
   21: </ul> 
   22:  
   23: </asp:Content>

In line two, we set up the Model type to be IQueryable<SuperEmployee>… notice this is exactly what my DomainService returned.

The in lines 15-18 I get strongly typed access to each SuperEmployee.

Here is how it looks:

image

Next, let’s look at the controller action for the Details view..

    1: public ActionResult Details(int id)
    2: {
    3:     var q = domainService.GetSuperEmployees();
    4:     var emp = q.Where(e=>e.EmployeeID==id).FirstOrDefault();
    5:  
    6:     return View(emp);
    7: }

Again, very simple, here we do a Linq query over the results of calling our business logic.  The cool thing about the composition of Linq is that this where clause passes from here, to the DomainSerivce, to the Entity Framework model and all the way to the database where it is executed as efficient tsql.  

The Details.aspx view is just as simple as the view above, basically just accessing the model.

image

Ok – so the read case is easy, what about update? 

Let’s look at the Edit action in the controller..

    1: public ActionResult Edit(int id)
    2: {
    3:     var q = domainService.GetSuperEmployees();
    4:     var emp = q.Where(e => e.EmployeeID == id).FirstOrDefault();
    5:  
    6:     return View(emp);
    7: }

Again, very similar to the Details action we saw.. this simply populates the fields.

Now let’s take a look at edit.aspx view for this action. We need a simple data entry form..

image

First, we add a Validation summary at the top of the form.  This is where we will display all the validation issues for the page.

    1: <%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>

Next, we include some standard HTML for each field we need filled out:

    1: <p>
    2:     <label for="Name">Name:</label>
    3:     <%= Html.TextBox("Name", Model.Name) %>
    4:     <%= Html.ValidationMessage("Name", "*") %>
    5: /p>
    6: <p>
    7:     <label for="Name">Gender:</label>
    8:     <%= Html.TextBox("Gender", Model.Gender) %>
    9:     <%= Html.ValidationMessage("Gender", "*")%>
   10: </p>

Notice in line 4 and line 9 we are adding validation hooks…

image

If there are any fields in the type that we don’t want to display, we still need to include them here so they will be in the postback data…

 <p>
 <%= Html.Hidden("EmployeeID", Model.EmployeeID)%>
 </p>

The other thing we want in postback data the set of unedited “original” data.. this is so that we can do concurrency checks with the database.

 <%= Html.Hidden("orginal.EmployeeID", Model.EmployeeID)%>
 <%= Html.Hidden("orginal.Gender", Model.Gender)%>
 <%= Html.Hidden("orginal.Issues", Model.Issues)%>
 <%= Html.Hidden("orginal.LastEdit", Model.LastEdit)%>
 <%= Html.Hidden("orginal.Name", Model.Name)%>
 <%= Html.Hidden("orginal.Origin", Model.Origin)%>
 <%= Html.Hidden("orginal.Publishers", Model.Publishers)%>
 <%= Html.Hidden("orginal.Sites", Model.Sites)%>

Notice here the naming convention of original.blahh.. as you will see later, this matches the name of the argument on the controller action.  Let’s flip back to the controller action and take a look..

    1: [AcceptVerbs(HttpVerbs.Post)]
    2: public ActionResult Edit(SuperEmployee emp, SuperEmployee orginal)
    3: {
    4:  
    5:     if (ModelState.IsValid)
    6:     {
    7:         domainService.AssociateOriginal(emp, orginal);
    8:         domainService.UpdateSuperEmployee(emp);
    9:         return RedirectToAction("Details", new { id = emp.EmployeeID });
   10:     }
   11:    
   12:    return View(emp);
   13:  
   14: }

Notice my method takes two arguments, emp, which is the current employee as it has been updated from the form and the “original” employee that is mapped back from the hidden html fields. 

Then in line 7, we associate the original value with the employee… this effectively makes it so that calls in the DomainService class to ChangeSet.GetOriginal() will return the right value.  Next we call UpdateSuperEmployee() on the business logic.  Here we do all the validation defined for the model including running custom validation code. If validation fails, the error information will be shown on the form.  Otherwise the data is eventually saved to the database.

What I have show is building an ASP.NET MVC application that shares the exact same application logic as my Silverlight client.  This includes things such as data validation that shows right through to the UI.    To bring it home, here is an example of the exact same validation error first in the Silverlight client, then in the ASP.NET MVC client…

image                     

                    image