Thursday, April 28, 2011

Create CRUD UI using ASP.NET 4.0, FormView, Dynamic Data, DomainDataSource, Entity Framework, WCF Domain Service, LINQ, and custom validation – Part I

This is one entry in a series of blog entries:

The title is a mouthful and thus the topic can be kind of overwhelming. With ASP.NET 4 there are so many different technology frameworks that you need to be familiar with. The problem that I have found is that most examples only use a one or two of these frameworks. In my world, I want to use them all and the examples are never as simple as the examples. In this blog entry, I will give a read world example of how to combine all these technologies. I find myself having to figure out how to do this over and over again. Hopefully, this will be of help to others.

This blog entry is NOT about how to create a Dynamic Data project in VS2010 and create an admin UI or something like that. This is about creating a page from scratch that uses this technology. To better understand the different scenarios that Dynamic Data can be used in and steps on how to extend it, see here.

Technology Overview

I am writing in the context of ASP.NET 4.0 and VS2010 (Visual Studio 2010). These technologies are relatively new in some cases and the api’s have changed since betas. I am only showing stuff that works in VS2010 (the final released version).

  • ASP.NET 4.0 – This is the version of ASP.NET that I am targeting.
  • FormView – This is the control that we will be using as a basis for the CRUD UI (User Interface).
  • Dynamic Data – This is the technology that allows you to use DynamicControl on your FormView instead of the standard ASP.NET Forms controls. BTW, you don’t have to have anything installed (not even the Dynamic Data files and templates). You get basic implementation without them, but with them you can customize and get a more full-featured set of tools. See here for a GREAT video on using Dynamic Data in “old” applications that don’t have all these cool technologies. For the most up to date resources, click here.
  • Entity Framework 4.0 – The standard with VS2010 for database to object mapping. Click here if you want to see how to use Dynamic Data with an ObjectDataSource and GridView.
  • WCF Domain Service – This is your middle tier where you would put your business logic, complex validation, etc. It provides a consist way to access your perform CRUD operations on your data. Abstraction layer before the Entity Framework.
  • LINQ – We will use LINQ to Entities in the WCF Domain Service to query database through the Entity Framework.
  • DomainDataSource – This is a control that works much like the ObjectDataSource or LinqDataSource or EntityDataSource except that it can connect to your WCF Domain Service from ASP.NET. Please note that there is also one for Silverlight and it has the same name so when you are Googling be sure to check what the context is. This is a good doc on using this control.

Getting Started

You can select most any type of web application project in VS2010 to get started. My instructions will be for the Dynamic Data project so that I don’t have to explain how to manually move over the Dynamic Data files. If you have an existing project and need to copy over the Dynamic Data files, click here for detailed instructions. Please note, I have copied some of these steps (and written others) from instructions I have read on MSDN, but have combined them together in one continuous instruction set. For example, most of the instruction are from: here and here.

  1. Open Visual Studio 2010 and create a new project of type ASP.NET Dynamic Data Domain Service Web Application. I’m calling my project MyDynamicDataExample.
  2. Optional - Create two Directories: Models and Services.
  3. Optional - Download and install the AdventureWorksLT database if you don’t already have it. The particular database is not really that important to understand the concepts. Notice I am using the Lite version since it is a bit simpler to follow, etc.
  4. Add a New Item… and select ADO.NET Entity Data Model. I called MyModel.edmx. We will generate from Database. Select the database you want to work with (add a new connection if it is not there). On the Choose Your Database Objects screen, select Product and ProductCategory. Keep everything else default.
  5. You must build your project so that the next step will work.
  6. Add (to Services folder) a New Item… and select Domain Service Class. I called mine AWDomainService.cs. Select the options as shown below:
    image
  7. Add (to Models folder) a New Item… and select Web Form using Master Page. Call it DDTest.aspx. Select the Site.master on the next screen.
  8. From the General or Data group of the Toolbox, add a DomainDataSource control to the page. If you have never used the DomainDataSource before you will need to add it to your toolbox. To add it to your toolbox. Click the Tools menu, and then click Choose Toolbox Items. In the Choose Toolbox Items dialog box, click the .NET Framework Components tab, select the DomainDataSource check box, and then click OK.
  9. Add a DomainDataSource to the page. Configure the datasource to point to GetProducts() method on the service you created. Enable Inserts, Updates and Deletes.
  10. Add a QueryExtender to the page. Set the TargetControlID to the id of the DomainDataSource (mine is called DomainDataSource1).
  11. Add a FormView (though a GridView or DetailsView could also be used). Check the Enable Dynamic Data Support checkbox. Please note it will not stay checked when you come back to it later (in some cases). Set the Data Source to the DomainDataSource you added earlier.
  12. Add a DynamicDataManager control to the page. Chose Register Controls… and add the FormView as shown below.
    image
    This registers the data-bound control and enables dynamic behavior for it.

  13. Open the code-behind. Add a Page_Init method and add the following line:

    protected void Page_Init(object sender, EventArgs e)
    {
        FormView1.EnableDynamicData(typeof(MyDynamicDataExample.Models.Product));
    }

  14. Go to the .aspx page again and go to the Source view. Find the QueryExtender you added earlier. Add the following line to it:

    <asp:QueryExtender ID="QueryExtender1" runat="server" TargetControlID="DomainDataSource1">
            <asp:DynamicRouteExpression ColumnName="ProductID" />
    </asp:QueryExtender>

    This will allow us to pass the ID that we want to edit via the url as a query string.

  15. Now we need to do some clean up. In each of the templates in the FormView remove the Text and Dynamic controls for: ProductCategoryReference, EntityState, and EntityKey. Also, remove the rowguid from the insert template.

  16. Let’s test to see if this works now. You will see that you get the YSOD (Yellow Screen Of Death) when you click the Edit button and then the Update button. You will get an EntityOperationException and the last method that was called was HandleValidationErrors. This means that something in the model failed.

  17. Create event handlers on the DomainDataSource for the Updated and Inserted events. They should look like this when you are done:

    protected void DomainDataSource1_Inserted(object sender, Microsoft.Web.UI.WebControls.DomainDataSourceStatusEventArgs e)
    {
        if (e.ChangeSetEntry.HasError)
        {
            foreach (var error in e.ChangeSetEntry.ValidationErrors)
            {
                AddValidationSummaryItem(error.Message);
            }
        }
    }

    protected void DomainDataSource1_Updated(object sender, Microsoft.Web.UI.WebControls.DomainDataSourceStatusEventArgs e)
    {
        if (e.ChangeSetEntry.HasError)
        {
            foreach (var error in e.ChangeSetEntry.ValidationErrors)
            {
                AddValidationSummaryItem(error.Message);
            }
        }
    }

  18. Next add this supporting method.

    public void AddValidationSummaryItem(string errorMessage)
    {
        var validator = new CustomValidator();
        validator.IsValid = false;
        validator.ErrorMessage = errorMessage;
        this.Validators.Add(validator);
    }

  19. Now put a breakpoint on the Updated event handler and debug your application. Do the same test again, you’ll see there is some error about the field: ThumbnailPhoto. Note that the error does not display on the page, but it doesn’t go to the YSOD either. Like any other validation exception that is NOT column specific, we need to use the ValidationSummary control to view this error.

  20. Drag a ValidationSummary control to your page. Re-run and you’ll see the exception. This does give little bit of database schema information. This may be considered by some to be bad. I’ll leave that up to you. If you don’t do this then you won’t get validation exceptions that are thrown from your domain service which is how you will implement your business logic. My experience shows that nothing too serious comes out of here once you have everything wired up correctly. If you don’t like that you’ll have to come up with another solution.

  21. Let’s quickly fix this by removing the ThumbNailPhoto field from each of the FormView templates. Re-run, and this time you won’t get any errors. We can do this because it is an optional field.

  22. Let’s add some validation to see how to do that. Go to the AWDomainService.cs and find the UpdateProduct method. Modify it so that it looks like this:


    public void UpdateProduct(Product currentProduct)
    {
        this.ObjectContext.Products.AttachAsModified(currentProduct, this.ChangeSet.GetOriginal(currentProduct));
        if (currentProduct.ListPrice < 2000)
            throw new ValidationException("List Price is too low.");
    }
    Re-run, change the List Price to less than 2000 and update and you’ll get the message shown in the ValidationSummary control.

  23. You can also use ValidationAttributes such as Required, Range, RegularExpression, etc in the Model to declaratively validate the data. To do this open your AWDomainServices.metadata.cs. Find the StandardCost property and modify it so that it looks like this:

    [Range(0, 200, ErrorMessage="{0} must be between 0 and 200.")]
    public decimal StandardCost { get; set; }

    Re-run and and change the standard cost to something greater than 200 if it isn’t already and Update. You’ll see it says StandardCost must be between 0 and 200. Notice it filled in the name of the field for us. It is the actual property name though.

  24. To display a user-friendly version of the property name you can add Display information as shown below.

    [Display(Name="Standard Cost")]
    [Range(0, 200, ErrorMessage="{0} must be between 0 and 200.")]
    public decimal StandardCost { get; set; }

    Re-run and you’ll see the same message but with the friendly name of the property.

  25. To Edit a particular record such as the Product with ID = 800, just go to the same url, but with DDTest.aspx?ProductID=800.

  26. If you try to run your application without specifying the specific test page you will get an error like this “There are no accessible tables. Make sure that at least one data model is registered in Global.asax and scaffolding is enabled or implement custom pages.”

    To fix that you just need to go to your Global.asax.cs and uncomment the line that start with DefaultModel.RegisterContext. Change it so that it looks like this:

    DefaultModel.RegisterContext(new DomainModelProvider(typeof(MyDynamicDataExample.Services.AWDomainService)), new ContextConfiguration() { ScaffoldAllTables = true});

    When you do this you’ll have some CRUD pages for each of the tables you included. This is separate from what we are doing here which is a custom page, but let’s fix the errors anyway. Another option would be to not have this default page and leave that line above commented. That way you won’t have all these admin page open to the world. Yes you can secure them, but they are not by default.

  27. Back on track, when you access the Default.aspx page you will get an error like this: The method 'Skip' is only supported for sorted input in LINQ to Entities. The method 'OrderBy' must be called before the method 'Skip'.

    To fix this just go to the AWDomainService.cs and add a sort method to the default query methods which are GetProducts and GetProductCategories. Just make the methods look like these:

    public IQueryable<ProductCategory> GetProductCategories()
    {
        return this.ObjectContext.ProductCategories.OrderBy(o => o.Name);
    }

    public IQueryable<Product> GetProducts()
    {
        return this.ObjectContext.Products.OrderBy(o => o.Name);
    }

  28. Re-run. This time go to the Product link. The same validation we added before is also here. This is because the validation is written at the Model, not the UI. This is AWESOME in my opinion. I love it. This is why I love Dynamic Data.

I hope this has been beneficial to all.

The entire source for this example (minus the AdventureWorksLT database) can be downloaded from here.

No comments: