Minggu, 23 Januari 2011

Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 4: SEO, Export to Excel and Out of Browser


More from my Mix09 talk “building business applications with Silverlight 3”.


You can watch the original video of the full session


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 and check out the running application.


Today, we will talk about Different views of our application logic.


Different Views


Now, let’s take take a look at putting different views on this same application. One of the key elements to supporting different views is deeplinking. Let’s look at adding that to this application.


image


First, we need to add a textbox to display the deeplink for each of our super employees.



<TextBlock Text='PermaLink:'></TextBlock>
<TextBox x:Name='PermalinkTextBox' Width='400' Height='25' TextWrapping='NoWrap' Foreground='#FFB4B4B4' />

And just wire this up in code behind..



private void dataGrid1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var emp = dataGrid1.SelectedItem as SuperEmployee;
    if (emp != null)
    {
        PermalinkTextBox.Text = Application.Current.Host.Source.ToString().Replace('ClientBin/MyApp.xap', '') +
            '#/Home?EmpId=' + emp.EmployeeID;
    }    
 
}

image_thumb[98]


Now we need to make the page smart enough to know about this deep link..



protected override void OnNavigatedTo(NavigationEventArgs e)
{
    var qs = NavigationContext.QueryString;
 
    if (qs.ContainsKey('EmpId'))
    {
        dds.FilterDescriptors.Add(
            new FilterDescriptor('EmployeeID',
                FilterOperator.IsEqualTo, qs['EmpId']));
    }
}

Now, run it… cut and paste this url into a browser brings up exactly this record.


image_thumb[100]


OK, now that we have a deeplink, what can we do with it? Well, you can send the link around in email, IM and in blogs.


But what if you wanted the search engines to be able to find all the items in your site? It turns out there is a standard format for this called a sitemap (http://sitemap.org). Check out http://amazon.com/robots.txt for an example in the real world.


To add this to our application, say File\New Item in the Server Project and select the “Search Sitemap”.


image_thumb[71]


This added a robots.txt and a sitemap.aspx. Let’s look at customizing the sitemap.aspx file. Notice we are using a new ASP.NET datasource control… that is designed to work with the DomainService class. This gives us server access to the same application logic from Silverlight and ASP.NET.



<asp:DomainDataSource runat='server' ID='SitemapDataSource' 
    DomainServiceTypeName='MyApp.Web.SuperEmployeeDomainService' 
    SelectMethod='GetSuperEmployees' />


<url>
    <loc><%= new Uri(this.Request.Url, this.ResolveUrl('Default.aspx'))+ '?EmpId='  %><%# HttpUtility.UrlEncode(Eval('EmployeeID').ToString()) %></loc>
    <lastmod><%# (Eval('LastEdit') as DateTime?).GetValueOrDefault().ToString('yyyy-MM-dd')%></lastmod>
</url>

One last tweak to enable the server side of the app to access the employee ID.. in default.aspx.cs add:



protected void Page_Load(object sender, EventArgs e)
{
    string empId = Request.QueryString['EmpId'];
    var deepLink = '/Home?EmpId=' + empId;
 
    if (empId != null)
    {
        Response.Write("<script type=text/javascript>window.location.hash='#" + deepLink + "';</script>");
    }
 
 
}

Now run it…


image_thumb[73]


and the sitemap…


image_thumb[104]


Cut and pasting the URL brings open that item.


image_thumb[103]


OK… that works great. Now the search engine can find every entity in our application. But what will the search engine find when it gets there? Search engines are great at parsing html, but dynamic content like we are seeing in this site or in rich ajax sites are a lot harder for search engines to deal with.


image_thumb[82]


The art of addressing this is called SEO (Search Engine Optimization). Let’s go and apply SEO principles to our application, open up BusinessApplicationTestPage.aspx Because we have modeled our application logic in a domain service class it is very easy to add a standards based HTML view. Just add this code into the object tag in default.aspx..



<form id='form1' runat='server' style='height:100%'>
             <asp:DomainDataSource runat='server' ID='DetailsDataSource' 
                       DomainServiceTypeName='MyApp.Web.SuperEmployeeDomainService' 
                       SelectMethod='GetSuperEmployee'>
              <SelectParameters>
                  <asp:QueryStringParameter  Name='employeeID' Type='Int32' Querystringfield='EmpId' />
              </SelectParameters>       
          </asp:DomainDataSource>
         <asp:ListView ID='EmployeeDetails' runat='server'  DataSourceID='DetailsDataSource' >
            <LayoutTemplate>                  
              <asp:PlaceHolder ID='itemPlaceholder' runat='server'></asp:PlaceHolder>    
            </LayoutTemplate>
            <ItemTemplate>
               <h1><a href="<%# Eval("EmployeeID")%>" > <%# Eval('Name')%> </a>  </h1>
                 <table>
                 <tr> <td><b>Name:</b> <%# Eval('Name')%> </td> </tr>
                 <tr> <td><b>Publisher:</b> <%# Eval('Publishers')%> </td></tr>
                 <tr> <td><b>Gender:</b> <%# Eval('Gender')%></td> </tr>
                 <tr> <td><b>Origin:</b> <%# Eval('Origin')%></td> </tr>
                 <tr> <td><b>Issues:</b> <%# Eval('Issues')%></td> </tr>
                 <tr> <td><b>Sites: </b><%# Eval('Sites')%></td> </tr>
              </table>
            </ItemTemplate>
         </asp:ListView>
 
 

We need add a GetSuperEmployee domain method to support this code.. so back to our SuperEmployeeDomainService and add:



 
        public SuperEmployee GetSuperEmployee(int employeeID)
        {
            return this.Context.SuperEmployeeSet
                       .Where(emp => emp.EmployeeID == employeeID)
                       .FirstOrDefault();
        }

Now run it… Disable Silverlight in the browser because hey, search engines don’t have silverlight installed.


image


Now hit refresh on a deep link…


image


Just for kicks… let’s run this application in lynx


image


image


Pretty cool, huh? Now the search engine (or anyone without Silverlight) can access the data.


But does it work? well, try these searches:


Super employee placement Alfred (Yahoo)


Super Employee Placement Alfred (some other guy)


OK, that was a server view, let’s try a different client view.. Let’s export this data to the worlds best data manipulation tool – excel!


Add the template file to the project to the client root.


Then add an “export to excel” button:



<Button Content='Export to Excel' 
        Width='105' Height='28'
        Margin='5,0,0,0' HorizontalAlignment='Left'
        Click='ExportToExcel_Click' ></Button>

And write out the excel file… Notice this is all safe access as the Silverlight runtime proxies the user selection of the location to write the file. The developer can not get direct access to the file location.



private void ExportToExcel_Click(object sender, RoutedEventArgs e)
{
    var context = dds.DomainContext as SuperEmployeeDomainContext;
    var s = Application.GetResourceStream(new Uri('excelTemplate.txt', UriKind.Relative));
    var dialog = new SaveFileDialog();
 
    dialog.DefaultExt = '*.xml';
    dialog.Filter = 'Excel Xml (*.xml)|*.xml|All files (*.*)|*.*';
 
    if (dialog.ShowDialog() == false) return;
 
    using (var sw = new StreamWriter(dialog.OpenFile()))
    {
        var sr = new StreamReader(s.Stream);
        while (!sr.EndOfStream)
        {
            var line = sr.ReadLine();
            if (line == '***') break;
            sw.WriteLine(line);
        }
 
        foreach (var emp in context.SuperEmployees)
        {
            sw.WriteLine("<Row>");
            sw.WriteLine("<Cell><Data ss:Type=\"String\">{0}</Data></Cell>", emp.Name);
            sw.WriteLine("<Cell><Data ss:Type=\"String\">{0}</Data></Cell>", emp.Origin);
            sw.WriteLine("<Cell><Data ss:Type=\"String\">{0}</Data></Cell>", emp.Publishers);
            sw.WriteLine("<Cell><Data ss:Type=\"Number\">{0}</Data></Cell>", emp.Issues);
            sw.WriteLine("</Row>");
        }
        while (!sr.EndOfStream)
        {
            sw.WriteLine(sr.ReadLine());
        }
    }
}
    }

Run it…


image_thumb[83]


image


This creates an excel file on the user selected location.


image


Opening it and doing a bit of formatting…


image


And the final view… out of browser.


Select properties on the client application


image_thumb[85]


Edit the settings..


image_thumb[86]


Run the application, right click and select “Install….”


image_thumb[88]


image_thumb[89]


Put a link on the desktop..


image


and run it!


image_thumb[90]

Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 12: DataSet


More from my Mix09 talk “building business applications with Silverlight 3”. Many customers have told me that they love Entity Framework and LinqToSql, but that they are not always able to use them in their projects just yet. In fact the number of folks that are using ADO.NET DataSet, DataReader, etc is very high. So I wanted to show taking my Mix demo and changing it to use the standard ADO.NET classic model of data access.


This allows you to use DataSet with Silverlight AND take advantage of all the cool new features RIA Services offers around data validation, paging, etc.


For the context, you can watch the original video of the full session


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 and check out the running application.


First, we can remove the Entity Framework model from our project…. we are going to use DataSet as our data access model in this demo. Notice this pattern likely makes the most sense if you already have a lot of infascture built up around DataSet… if not, then using DataReader\Writer might be a good choice.


First, we need to create a type that we return to the client.



public class SuperEmployee
{
 
    [ReadOnly(true)]
    [Key]
    public int EmployeeID { get; set; }
 
 
    [RegularExpression('^(?:m|M|male|Male|f|F|female|Female)$',
        ErrorMessage = 'Gender must be 'Male' or 'Female'')]
    public string Gender { get; set; }
 
    [Range(0, 10000,
        ErrorMessage = 'Issues must be between 0 and 1000')]
    public Nullable<int> Issues { get; set; }
 
    public Nullable<DateTime> LastEdit { get; set; }
 
    [Required]
    [StringLength(100)]
    public string Name { get; set; }
 
    public string Origin { get; set; }
 
    public string Publishers { get; set; }
 
    public string Sites { get; set; }
}

Notice here that we are able to put the validation metadata directly on the type we are returning. Now we just need to fill up this type from the database..


Let’s start by defining a DomainService



   1: [EnableClientAccess()]
   2: public class SuperEmployeeDomainService : DomainService
   3: {
   4:     DataSet Context = new DataSet();
   5:  
   6:     const int PageSize = 20;
   7:  

Notice here we are driving directly from DomainService… there is no need to use the EF or L2SDomainService.. We then setup the Context to be a DataSet.. we will populate this DataSet in the methods on the DomainService. Then we define a PageSize for our data.. this gives us a standard chunk to access from the database.


Then I wrote some fairly simply code to deal with populating the DataSet… I’d guess it would be easy to change this to work with whatever pattern you are using to full up DataSets today.



void FillSuperEmployees(DataSet ds, int page, int employeeID)
{
    var conn = new SqlConnection();
    conn.ConnectionString = ConfigurationManager.ConnectionStrings['MainConnStr'].ConnectionString;
 
    SqlDataAdapter da;
    if (employeeID == -1)
    {
        da = new SqlDataAdapter(
            'SELECT * ' +
            'FROM SuperEmployees',
            conn);
    }
    else
    {
         da = new SqlDataAdapter(
            'SELECT * ' +
            'FROM SuperEmployees ' +
            'WHERE EmployeeID=' + employeeID,
            conn);
    }
    if (page == -1) da.Fill(ds, 'SuperEmployees');
    else            da.Fill(ds, page * PageSize, PageSize, 'SuperEmployees');
}
 

Next we write a query method..



   1: public IQueryable<SuperEmployee> GetSuperEmployees(int pageNumber)
   2: {
   3:     Context = new DataSet();
   4:     FillSuperEmployees(Context, pageNumber,-1);
   5:     DataTable superEmployees =
   6:         Context.Tables['SuperEmployees'];
   7:  
   8:     var query = from  row in 
   9:                     superEmployees.AsEnumerable()
  10:             select new SuperEmployee
  11:             {
  12:                EmployeeID = row.Field<int>('EmployeeID'),
  13:                Name = row.Field<string>('Name'),
  14:                Gender = row.Field<string>('Gender'),
  15:                Issues = row.Field<int?>('Issues'),
  16:                LastEdit = row.Field<DateTime>('LastEdit'),
  17:                Origin = row.Field<string>('Origin'),
  18:                Publishers = row.Field<string>('Publishers'),
  19:                Sites = row.Field<string>('Sites'),
  20:             };
  21:     return query.AsQueryable();
  22: }

In line 4 we fill up the DataSet then in line 8-20, we use some LinqToDataSet support to make it easier to create a projection of our DataSet. If you’d rather not use Linq here, no problem, you can simply write a copy method to such the data out the DataSet and into our SuperEmployee type. Any collection can be returned as an IQuerable. Notice we are taking the page number here.. we are going to follow the same explicit paging pattern I introduced in the WCF example.


Then let’s take a look at Update… this method is called when there is a change to one of the fields in our SuperEmployee instance…



   1: public void UpdateSuperEmployee(SuperEmployee currentSuperEmployee)
   2: {
   3:  
   4:     GetSuperEmployee(currentSuperEmployee.EmployeeID);
   5:  
   6:     DataRow updateRow = null;
   7:     foreach (DataRow row in Context.Tables['SuperEmployees'].Rows) {
   8:        if (row.Field<int>('EmployeeID') == currentSuperEmployee.EmployeeID) {
   9:            updateRow = row;
  10:        }
  11:     }
  12:    
  13:     var orgEmp = this.ChangeSet.GetOriginal(currentSuperEmployee);
  14:   
  15:     if (orgEmp.Gender != currentSuperEmployee.Gender)
  16:         updateRow.SetField('Gender', currentSuperEmployee.Gender);
  17:     if (orgEmp.Issues != currentSuperEmployee.Issues)
  18:         updateRow.SetField('Issues', currentSuperEmployee.Issues);
  19:     if (orgEmp.LastEdit != currentSuperEmployee.LastEdit)
  20:         updateRow.SetField('LastEdit', currentSuperEmployee.LastEdit);
  21:     if (orgEmp.Name != currentSuperEmployee.Name)
  22:         updateRow.SetField('Name', currentSuperEmployee.Name);
  23:     if (orgEmp.Origin != currentSuperEmployee.Origin)
  24:         updateRow.SetField('Origin', currentSuperEmployee.Origin);
  25:     if (orgEmp.Publishers != currentSuperEmployee.Publishers)
  26:         updateRow.SetField('Publishers', currentSuperEmployee.Publishers);
  27:     if (orgEmp.Sites != currentSuperEmployee.Sites)
  28:         updateRow.SetField('Sites', currentSuperEmployee.Sites);
  29:     
  30: }

First we need to get the DataRow to update. In line 4, we load it up from the Database, then in line 6-11 we find it in the current DataSet (remember, we are doing batch processing so their could be several updates already done in the the DataSet).


Notice the general pattern here is that we compare the original results that the client last saw (from line 13) to what is being sent up from the client. This ensure that we only change the fields that are actually updated. Otherwise we could overwrite another clients changes. This is very much like the code we did in the DTO example.


Finally, in Submit, we need to actually commit these changes to the database.



   1: public override void Submit(ChangeSet changeSet)
   2: {
   3:     base.Submit(changeSet);
   4:     var conn = new SqlConnection();
   5:     conn.ConnectionString = ConfigurationManager.ConnectionStrings['MainConnStr'].ConnectionString;
   6:  
   7:  
   8:     SqlDataAdapter da = new SqlDataAdapter(
   9:         'SELECT * ' +
  10:         'FROM SuperEmployees ',
  11:         conn);
  12:     SqlCommandBuilder com = new SqlCommandBuilder(da);
  13:     da.Update(Context, 'SuperEmployees');
  14:  
  15: }

Looking at this Submit override gives us some good insights into how RIA Services really works. Submit is called when a request first comes in from the client. It could contain several adds, deletes or updates in the changeSet. calling base.Submit() breaks the changeset out and calls the appropriate update\add\delete methods for each change. Those changes should leave the DataSet populated with the changes we need to commit to the database. Line 13 takes care of that. Notice this is also a really good place to set a breaking point when you are trying to debug your DomainService.


The only real changes to the client are to accommodate the explicit paging pattern we saw in the WCF example… which is great.. that means you can move from this DataSet model, to EF with very minimal changes to the client.


This example showed how to use existing code dealing with DataSet and expose it to Silverlight clients via .NET RIA Services.


Enjoy!

Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 13: The New Class Library Project


Well… this has been one of the longer blog series I have done, but there is so much meat here! Maybe someone will write a book or two? Anyway, in this post, I wanted to spend a little time looking at the project structure we have been working with and see if there is a way to improve it a bit with more clear separation of concerns.


For those of you just joining it, this is yet more updates to my Mix09 talk “building business applications with Silverlight 3”. You can watch the original video of the full session


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


The core project structure we have been using so far is a single solution with two projects in it:



MyApp – The Silverlight App


MyApp.Web – the web application


The MyApp.Web project actually has two very distinct concerns.. the first is handing the AppLogic for the entire app, the 2nd is the web and services interfaces (specifically the default.aspx down level rendering, sitemap.aspx and SuperEmployeeService.svc). Two concerns in the same project bother me at some level as I tend to like a cleaner separation for maintenance and better cross team work. Luckily, new in the July update to RIA Services is a new RIA Services Class Library project. We will look at refactoring the MyApp.Web project to pull out the appLogic concerns into a separate project.


Right click on the solution and select “Add New Project….”


image


This creates a pair of class library projects.. one for the client and one for the server.


image


First you will need to add a reference from the MyApp.Web project to the SuperEmployeeRIAServicesLibrary.Web.. We are going to want to access the DomainService defined in this library from several places in the WebApp.


Now, simply add an Entity Framework model and SuperEmployeeDomainService to the SuperEmployeeRIAServicesLibrary.Web project exactly as we way back in step 2. Once you have the SuperEmployeeDomainService class created, you can copy over the implementations we did thus far… remember to grab the cool metadata validation attributes as well.


Then you can delete the Domain services classes from the MyApp.Web project. This gives you a very clean set of concerns..


image


Notice that SuperEmployeesRIAServicesLibrary.Web contains the datamodel and Domain classes and the MyApp.Web project has the asp.net pages and the service. All that access the DomainModel from the class library project.


Notice if you are using the “Business Application Template” as I am in this walk through, you will notice a Services folder that includes some DomainServices for dealing with user management. In some ways, I think of this as an application wide concern, so it might make sense to leave these in the web project, but if you’d like to move them, no problem. Just add the following steps:



1. Move the entire directly to the SuperEmployeeRIAServicesLibrary.Web project. It is likely a bit cleaner to update the namespaces in these types, if you do so, you will need to update the namespaces on the client as well.


2. Break the “RIA Services Link” from the Silverlight client to the MyApp.Web project in the MyApp properties page.
image


3. Add a project reference from MyApp to SuperEmployeesRIAServicesLibrary


4. Add a simple RIAContext class to your application



public sealed partial class RiaContext : System.Windows.Ria.RiaContextBase
{
    partial void OnCreated();
 
    public RiaContext()
    {
        this.OnCreated();
    }
    public new static RiaContext Current
    {
        get
        {
            return ((RiaContext)(System.Windows.Ria.RiaContextBase.Current));
        }
    }
    
    public new User User
    {
        get
        {
            return ((User)(base.User));
        }
    }
}



5. Explicitly wire up the AuthDomainContext in App.xaml.cs with lines 5 and 6.


   1: private void Application_Startup(object sender, StartupEventArgs e)
   2: {
   3:     this.Resources.Add('RiaContext', RiaContext.Current);
   4:  
   5:     var auth = RiaContext.Current.Authentication as FormsAuthentication;
   6:     auth.DomainContext = new AuthenticationContext();
   7:     
   8:     this.RootVisual = new MainPage();
   9: }

Again, the above steps are optional – only if you want to factor out the DomainService completely from your web application.


Now we just need to update some namespace references across the silverlight client and the web because our DomainService class is in a new namespace. And presto! The apps works great, but now the project is better factored for long term maintenance.


Notice that every aspect of the app keeps working….


The Silverlight client..


image


The ASP.NET view (for SEO)..


image


The Service…


image


And of course the WinForms client for that service..


image