Kirk Evans Blog

.NET From a Markup Perspective

Using ASP.NET Dynamic Data with the Windows Workflow Foundation Rules Engine

Using ASP.NET Dynamic Data with the Windows Workflow Foundation Rules Engine

Rate This
  • Comments 2

In this post, I will show how to use the Windows Workflow Foundation rules engine to provide business logic for a Dynamic Data Entites Web Application.  We will show how to change business rules without modifying code, drive the application based on a logical entity model, and map the entity model to a data store.

I have to admit that I am in awe of how easy this was to do. 

Create the Dynamic Data Entities Web Application

To start, open Visual Studio 2008 with SP1 installed and create a new Dynamic Data Entities Web Application.  This will generate what seems at first to be a lot of code, but once you peek under the hood you will see that it's really just templating code that you have full control over.  I'm not going to explain all of Dynamic Data here, for more information you should check out the great getting started video series available at www.asp.net/dynamicdata.

Once you create the web application, add a new ADO.NET Entity Data Model.  I named mine "Northwind.edmx".  When propmpted, click OK to add the asset to the App_code folder.  Next, a wizard pops up asking you what the model should contain.  I chose "Generate from database" and used the Northwind database, choosing the Customers, Orders, and Order_Details tables.

image

Once you have the model created, you need to wire it up to the application.  Before we do this, make sure you download the Dynamic Data Entity Framework Workaround. In your ASP.NET web application, right-click and choose "Add ASP.NET Folder" and choose "Bin".  Then copy the workaround DLL into your Bin directory.  We use this DLL to wire up the model to our dynamic data application.  Open Global.asax and find the commented line starting with model.RegisterContext and change it to:

model.RegisterContext(new Microsoft.Web.DynamicData.EFDataModelProvider
(typeof(NorthwindModel.NorthwindEntities)), 
new ContextConfiguration() { ScaffoldAllTables = true }); 

The other change I made was to update the route.  Instead of using Edit.aspx for edits, I instead wanted to use ListDetails.aspx for a master/detail view, also allowing me to use the GridView control.  I edited the Global.asax to the following.

<%@ Application Language="C#" %>
<%@ Import Namespace="System.Web.Routing" %>
<%@ Import Namespace="System.Web.DynamicData" %>

<script RunAt="server">
    public static void RegisterRoutes(RouteCollection routes) {
        MetaModel model = new MetaModel();
        
        model.RegisterContext(new Microsoft.Web.DynamicData.EFDataModelProvider(typeof(NorthwindModel.NorthwindEntities)), new ContextConfiguration() { ScaffoldAllTables = true });

        
        // The following statements support combined-page mode, where the List, Detail, Insert, and
        // Update tasks are performed by using the same page. To enable this mode, uncomment the
        // following routes and comment out the route definition in the separate-page mode section above.
        routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx")
        {
            Action = PageAction.List,
            ViewName = "ListDetails",
            Model = model
        });

        routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx")
        {
            Action = PageAction.Details,
            ViewName = "ListDetails",
            Model = model
        });
    }

    void Application_Start(object sender, EventArgs e) {
        RegisterRoutes(RouteTable.Routes);
    }

</script>

You should be able to hit F5 and have a working application so far (make sure to set Default.aspx as the startup page for the web application).

Add Partial Types for Entities

There's probably a more elegant way to do this, but I needed a way to signal if an entity is valid and also to trap the validation message for the entity.  The easiest way to do this is to create a partial type for your entity class and add 2 properties, Valid and ValidationMessage.  We'll use these properties from our rules engine.

namespace NorthwindModel
{
    public partial class Customers :  EntityObject
    {
        public bool Valid { get; set; }
        public string ValidationMessage { get; set; }
        
    }

 

In hindsight, I probably could've created a common interface and used it for all of the entities.  There might even be something there for Entity Framework and Dynamic Data to automatically add this type of error in, I'll leave investigation of this approach as an exercise to the reader.

Publish the Web Application

The next step is to deploy the web application.  Right-click on the web project and choose "Publish Web Site".  In that screen, I checked "Emit debug information" because I wanted to make sure that I could step through the types during debugging.  Make sure to note the directory where you published the web site to, you will need this directory in a subsequent step.

image

Create the Rules Database

The next step is to download the External Ruleset Demo from the MSDN RuleSet Sample in the SDK.  This is a fantastic demo application for Windows Workflow Foundation that allows you to use the Windows Workflow Foundation rules engine in your application without using workflows.  Download the package and then run Setup.bat to create the Rules database.  Next, load up the ExternalRuleSetToolKit.sln into Visual Studio 2008 to convert the application from Visual Studio 2005 to Visual Studio 2008 format.  Once that's done, hit F5 to run the solution. 

Create Rules for Your Application

Once the RuleSetTool application is running, click the New button.  This will create a new ruleset.  Give it a name (I called mine "ValidateCustomer").  On the top right of the form, there is a button to browse to a selected workflow or type.  Click that browse button, then click browse again on the resulting "Workflow/Type Selection" dialog, and browse to the location of your published web application.  Under that folder, choose "App_Code.dll".

image 

After selecting the App_Code.dll, the dialog will show you a list of contained types and their members.  I chose the "NorthwindModel.Customers" entity type that was generated by the Entity Framework designer.

image

Once you select the type and click OK, the final screen looks like this.

image

The next thing to do is to add your rules.  This was the part where I stepped back and said "whoa, I can't believe this is so easy".  Click the "Edit Rules" button.  This is where you will actually define rules.  For instance, I added a rule "AddressIsValid" where I check to see if the Address is null or empty.  If it is, then I set Valid to false and set the ValidationMessage property to "Address is missing."  Similarly, I added a rule that checks the CompanyName to see if it contains a specific value and set Valid and ValidationMessage appropriately.

image

Once you define the rules, click OK.  On the main form, go to the "Rule Store" menu item and click Save.

Adding Custom Business Logic with Entity Framework

This one took awhile for me to find because I am not very familiar with Entity Framework.  A quick search yielded "How to: Execute Business Logic When Saving Changes".  This is where we evaluate the rules in our application.

Create a partial class for the context type.  Add a partial method OnContextCreated that provides a handler for the SavingChanges event.  In this event handler, add the following code.

public partial class NorthwindEntities
    {
        partial void OnContextCreated()
        {
            this.SavingChanges += new EventHandler(NorthwindEntities_SavingChanges);
        }

        void NorthwindEntities_SavingChanges(object sender, EventArgs e)
        {            
            // Validate the state of each entity in the context
            // before SaveChanges can succeed.
            foreach (ObjectStateEntry entry in
                ((ObjectContext)sender).ObjectStateManager.GetObjectStateEntries(
                EntityState.Added | EntityState.Modified))
            {
                // Find an object state entry for a SalesOrderHeader object. 
                if (!entry.IsRelationship && (entry.Entity.GetType() == typeof(Customers)))
                {
                    Customers customerToCheck = entry.Entity as Customers;
                    customerToCheck.Valid = true;

                    RuleSetService svc = new RuleSetService();
                    RuleSet ruleset = svc.GetRuleSet(new RuleSetInfo("ValidateCustomer"));
                    RuleExecution exec = new RuleExecution(new RuleValidation(customerToCheck.GetType(), null), customerToCheck);
                    ruleset.Execute(exec);
                    if (!customerToCheck.Valid)
                        throw new ArgumentException(customerToCheck.ValidationMessage);


                }
            }

        }

Note that this is where the Valid and ValidationMessage properties come into play.  We use this to determine if the rules fail and subsequently throw an exception.  This is how we signal to our application that there are business logic errors above the field validation that we get through dynamic data and entity framework.  However, now we have a problem... we need to handle the error in the application.

Handling the Validation Error in the Dynamic Data Application

This took me awhile to figure out, but the answer was really easy.  When in doubt, steal borrow someone else's code!  While researching how to handle asynchronous postback errors, I stumbled upon this example (http://asp.net/AJAX/Documentation/Live/mref/M_System_Web_UI_ScriptManager_OnAsyncPostBackError_1_3c0f9ecf.aspx).  I modified their example slightly, and ended up with the following code.  Open the master page, Site.master, and modify it to include script to handle the error.

<%@ Master Language="C#" CodeFile="Site.master.cs" Inherits="Site" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Dynamic Data Site</title>
    <link href="~/Site.css" rel="stylesheet" type="text/css" />
</head>
<body class="template" id="bodytag">
    <h1><span class="allcaps">Dynamic Data Site</span></h1>
    <div class="back">
        <a runat="server" href="~/"><img alt="Back to home page" runat="server" src="DynamicData/Content/Images/back.gif" />Back to home page</a>
    </div>

    <form id="form1" runat="server">
                <script type="text/javascript" language="javascript">
                var divElem = 'AlertDiv';
                var messageElem = 'AlertMessage';
                var errorMessageAdditional = 'Please try again.';
                var bodyTag = 'bodytag';
                Sys.WebForms.PageRequestManager.getInstance().add_endRequest(EndRequestHandler);
                function ToggleAlertDiv(visString)
                {
                     if (visString == 'hidden')
                     {
                         $get(bodyTag).style.backgroundColor = 'white';                         
                     }
                     else
                     {
                         $get(bodyTag).style.backgroundColor = 'yellow';                         

                     }
                     var adiv = $get(divElem);
                     adiv.style.visibility = visString;

                }
                function ClearErrorState() {
                     $get(messageElem).innerHTML = '';
                     ToggleAlertDiv('hidden');                     
                }
                function EndRequestHandler(sender, args) {                    
                    
                   if (args.get_error() != undefined && args.get_error().httpStatusCode == '500')
                   {
                       var errorMessage = args.get_error().message
                       args.set_errorHandled(true);
                       ToggleAlertDiv('visible');
                       $get(messageElem).innerHTML = '"' + 
                                errorMessage + '" ' + errorMessageAdditional;
                   }

                }
            </script>
            <div id="AlertDiv" style="visibility:hidden">
                <div id="AlertMessage" style="color:Red">
                </div>
           </div>
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true"  />
        
        <asp:ContentPlaceHolder id="ContentPlaceHolder1" runat="server">
        </asp:ContentPlaceHolder>
    </div>
    </form>
</body>
</html>

The modifications I made include getting rid of the "clear" button, changing the background color to yellow, and changing the error font to red.  Now, when an error occurs, the background is altered to signal to the user that there was a problem.  This also leaves the ASP.NET GridView in the edit mode.  One problem, though... how do we clear the error?  This stumped me for awhile, until I came across the obvious solution.  Simply add an onclick handler to the GridView in ListDetails.aspx.cs.

    protected void OnGridViewDataBound(object sender, EventArgs e) {
        if (GridView1.Rows.Count == 0) {
            DetailsView1.ChangeMode(DetailsViewMode.Insert);
        }
        GridView1.Attributes.Add("onclick", "ClearErrorState()");
    }

The Final Application

This is what is so cool... the whole application took me hardly any time at all and now I have business rules externalized from a fully functional application.  When I click on the Customers entity, I have a grid of customers.  Click edit, and change one of the values to have the rules engine set the Valid property to false.  This causes the background to turn yellow and the error to display with a red font.

image

What's also incredibly cool is the user experience.  The row is left in edit mode, the background is yellow, the font is red.  They just click anywhere in the GridView, and the background returns to white and the error message goes away.

image

Now, here's what blows me away.  With the web app still running, run the RuleSetTookit project again and change the rule.  For instance, I changed the IsCompanyValid rule to check that any customer with a country anywhere other than "United States" to also have a "Region" value.  Update the same record again, choosing the country as something other than United States, and the application will show another error.  This is because the rules are externalized from the application, they can be changed on the fly without modifying your code.

I'll admit that it took me about 2 hours to put this together in the middle of phone calls and having to research stuff.  I just don't get to code as much these days and my skills are rusty.  However, once I learned how to do everything, I can put the whole site together in about 6 minutes.  I should do the whole thing as a screencast just to show how little effort is involved to do this.

For More Information

www.asp.net/dynamicdata

Dynamic Data Entity Framework Workaround

External RuleSet Demo

How to: Execute Business Logic When Saving Changes

System.Web.UI.ScriptManager.OnAsyncPostBackError

Leave a Comment
  • Please add 3 and 5 and type the answer here:
  • Post
Translate This Page
Search
Archive
Archives