Minggu, 23 Januari 2011

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

Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 17: Evolving an Application


More from my Mix09 talk “building business applications with Silverlight 3”. So far in this series we have looked at how to build out an new application using Silverlight 3 and .NET RIA Services… but now let’s look at how to evolve an existing SL3\RIA Services application… after all, for most of us that is our day-to-day jobs. It is rare that we get to start with a green field application where we have full control. Being able to deal with maintenance of an application takes a framework from being a cool demo toy to being an important tool!

For the context, you can watch the original video of the full session and catch up on the Full series

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

Also, download the full demo files .

For this part, we will look at to interesting evolutions of the standard SuperEmployee app we have been building… The first is adding a new column to the table we have been working with and the second is to add a whole new table with additional data associated with each SuperEmployee.

A fair bit of the details of how the evolution workflow is accomplished is dependent on what DAL you use. Because .NET RIA Services is DAL agonistic, this part of the evolution story can change a bit. For this walkthrough I will show the story with Entity Framework and will leave it as an exercise for the reader to map it to your own DAL.

Adding a new Column

Ever since I cooked up the SuperEmployee schema 6 months ago I felt that it was really missing the most important aspect of being a super hero. That is knowing what his or her power is. I mean, i have to know their super power to hire them right? So let’s look at adding “Superpower” to the database.

Let’s open nortwind.mdf from the app_data directory and select Open Table Definition

image

Then add a new column called “SuperPower” and set it type to be nvarchar(MAX)

image

OK – we have the database scheme updated, now we need to update our model. To do that open northwind.edmx and select “Update Model from Database”

image

Select Refresh

image

And we see our model is updated!

image

Now, simply build and we can access this new property from the web server in the DomainService if we needed to write some application logic that deals with it or we can access it directly from the client.

image

To start with, let’s add SuperPower to the list of fields the DataForm is handling for us… This is just an update to the DataForm we talked about way back in Part 2: Rich Data Query

<dataControls:DataField>



    <TextBox Text="{Binding SuperPower, Mode=TwoWay}" />



</dataControls:DataField>




And of course the display works great



image



But I can now add a value to several SuperHeros and hit submit and see the values reflected in the database.



image



Now let’s add just a bit of validation to our new member.. In the SuperEmployeeDomainService.metadata.cs file add





[StringLength(25,MinimumLength=3,



    ErrorMessage = "Superpower's should be between 3 and 25 characters long")]



public string SuperPower;




build and run and we get get validation!



image



Let’s roll back to our Astoria service and our WinForm client from Part 5… what do we need to do to update those? Well, not much. The Astoria service is all dynamically generated based on the model and the DataGridView is also dynamic in this case, so all we need to do is update the service reference and we are good to go!



image



And the app runs great… an easy way to enter lots of data quickly..



image



What I showed so far is how easy it is to evolve your entity to have an addition field. Let’s now look at adding a whole new entity.



Adding a New Associated Entity



For this example, i’ll add some contact information for the SuperHero agent so that we can get up with the SuperHero we decide to hire. The first step is to add a table to our database to store the contact information



image



Then we need to associate it with the SuperEmployees table via an foreign key.



image



Now we can update our Entity Framework model



image



We want to select the new Contact class



image



And update our SuperEmployee class to get the FK for the Contact



image



That gives us this model



image



do a full build and everything should be good.



Now let’s integrate the Contact table into our app.



First we need to add the logic to our DomainService.





   1: public IQueryable<SuperEmployee> GetSuperEmployees()



   2: {



   3:     return this.Context.SuperEmployeeSet



   4:                .Include("Contact")



   5:                .Where(emp=>emp.Issues>100)



   6:                .OrderBy(emp=>emp.EmployeeID);



   7: }




Notice in line 4, we told EF to include the associated Contact item when we pull each SuperEmployee from the database.



Now in SuperEmployeeMetadata.cs we need to add some metadata to tell RIA Services to include the contact information in data it sends to the client





[Include]



public Contact Contact;




Now, we may want to edit this contact information, so let’s add methods to the DomainService to add new and update our Contact entity.





public void InsertContact(Contact contact)



{



    this.Context.AddToContact(contact);



}



 



public void UpdateContact(Contact currentContact)



{



 



    this.Context.AttachAsModified(currentContact,



                                  ChangeSet.GetOriginal(currentContact));



}




Great. The server side is done, now let’s go to the client.. Here what I want to do is add some UI to the details dataform to show the contact information.



Inside the DataForm in Home.xaml add a button to edit the contact information.





<Button Content="Edit Contact Information..." 



        Width="205" Height="28"



        Margin="15,10,0,0" HorizontalAlignment="Left"



        Click="EditContact_Click" >



</Button>




image



And we need to handle the click event. Here we create a ChildWindow to display a form to view\edit the contact information.





private void EditContact_Click(object sender, RoutedEventArgs e)



{



    var emp = dataForm1.CurrentItem as SuperEmployee;



    if (emp.Contact == null)



        emp.Contact = new Contact();



    var w = new EditContactWindow(emp.Contact);



    w.Show();



    w.Closed += EditContact_Closed;



 



}



 



void EditContact_Closed(object sender, EventArgs e)



{



    var win = sender as EditContactWindow;



    var emp = dataForm1.CurrentItem as SuperEmployee;



    if (win.DialogResult == true)



    {



        emp.Contact = win.Contact;



 



    }



}




Add a new ChildWindow and call it EditContactWindow(). In the codebehind set it up in code behind.





public partial class EditContactWindow : ChildWindow



{



    public Contact Contact;



    public EditContactWindow(Contact contact)



    {



        InitializeComponent();



        Contact = contact;



        this.LayoutRoot.DataContext = Contact; 



    }



 



    private void OKButton_Click(object sender, RoutedEventArgs e)



    {



        this.DialogResult = true;



        dataform1.CommitEdit();



    }



 



    private void CancelButton_Click(object sender, RoutedEventArgs e)



    {



        this.DialogResult = false;



        



    }



}




And the Xaml is simply a DataForm to display the results:





<dataFormToolkit:DataForm   x:Name="dataform1" CurrentItem="{Binding}">



    <dataControls:DataForm.EditTemplate>



        <DataTemplate>



            <StackPanel>



                <dataControls:DataField>



                    <TextBox Text="{Binding ContactName, Mode=TwoWay}" />



                </dataControls:DataField>



                <dataControls:DataField>



                    <TextBox Text="{Binding ContactTitle, Mode=TwoWay}" />



                </dataControls:DataField>



                <dataControls:DataField>



                    <TextBox Text="{Binding Address, Mode=TwoWay}" />



                </dataControls:DataField>



                <dataControls:DataField>



                    <TextBox Text="{Binding City, Mode=TwoWay}" />



                </dataControls:DataField>



                <dataControls:DataField>



                    <TextBox Text="{Binding Region, Mode=TwoWay}" />



                </dataControls:DataField>



                <dataControls:DataField>



                    <TextBox Text="{Binding PostalCode, Mode=TwoWay}" />



                </dataControls:DataField>



                <dataControls:DataField>



                    <TextBox Text="{Binding Phone, Mode=TwoWay}" />



                </dataControls:DataField>



            </StackPanel>



        </DataTemplate>



    </dataControls:DataForm.EditTemplate>



</dataFormToolkit:DataForm>     




And it looks great!



image



Notice, back on the server in SuperEmployeeDomainService.metadata.cs, i did give it some metadata to control how the Contact is displayed. This is where I would put validation as well.





[MetadataTypeAttribute(typeof(Contact.ContactMetadata))]



public partial class Contact



{



 



    internal sealed class ContactMetadata



    {



 



        // Metadata classes are not meant to be instantiated.



        private ContactMetadata()



        {



        }



 



        



        [Display(Name="Name")]



        public object ContactName;



        



        [Display(Name = "Title")]



        public object ContactTitle;



 



        public object Address;



        



        public object City;



        



        [Display(Name = "State")]



        public object Region;



        



        [Display(Name = "Zip Code")]



        public object PostalCode;



 



        public object Phone;



 



    }



}




We are done!



In this part we looked at how to incrementally evolve a RIA Services + Silverlight application. I showed how to add a new column to an existing table and how to add a whole new table.



Enjoy!