ASP.NET Web Forms 4.5, MVVM and Testability

22 בפברואר 2012

ASP.NET Web Forms 4.5 introduces a new data binding mechanism. The formal name of this feature is "Model Binding". I guess the name implies that pages/controls are now bound to a model object and not just plain data.


Is there any difference between data and model?


I guess that most of us will agree that model has state + behavior while data has only state. Taking that into account it is not surprising that the new data binding mechanism is named "Model Binding" since we can use it to bind to arbitrary methods and not just properties.


From flexibility perspective this is great news. But seriously, what are the practical reasons for using the new "Model Binding"?


I would say three main reasons:




  • Clean and simpler code


  • MVVM mode of operation


  • Better testability

In the following sections I would like to describe why the new model binding looks like MVVM and how we can improve testability by using it


Suppose we have a domain model object named "User" which logically has the following behavior: 




  • SetDetails(name, email)


  • Validate


  • SaveChanges

I am not going to show how this object is implemented since if we are doing MVVM correctly it should not matter to us.


The ViewModel should abstract the interesting state+behavior from the domain model in a way that is more compatible with our page/view. So, from page perspective the ViewModel should look something like that:

class UserViewModel
{
    public string Name { get; set; }
    public string EMail { get; set; }
}


We can now add all kind of metadata information that will allow us to implement the view/page as easily as we can (well, this is the whole purpose of a view model, right?)


For example, common task when writing a page is validation, so, lets add validation capabilities to the view model:


public class UserViewModel
{
    [
Required(ErrorMessage="Please specify non empty name")]
    public string Name { get; set; }

    [
DataType(DataType.EmailAddress)]
   
public string EMail { get; set; }
}


Once we have a convenient ViewModel we can implement the view/page. Let's add a new page named UserCreate.aspx with the following markup (some markup was removed to improve readability):

<asp:FormView ModelType="ViewModel" InsertMethod="InsertUser">
  <InsertItemTemplate>
    <span class="field-label">
      
Name:
    
</span>
   
<span class="field-editor">
     
<asp:TextBox ID="TextBoxName" Text='<%# BindItem.Name %>' />
   
</span>


    <span class="field-label">
     
E-Mail:
   
</span>


    <span class="field-editor">
     
<asp:TextBox ID="TextBoxEMail" Text='<%# BindItem.EMail %>' />
   
</span>


    <asp:ValidationSummary ShowModelStateErrors="true" />


    <asp:Button Text="Save" CommandName="Insert" />
  </InsertItemTemplate>
</asp:FormView>


As you can see the view is bound to the ViewModel through the "BindItem.Name" and "BindItem.EMail". This is a truely an MVVM mode of operation !!!


Inside the page we need to implement the "InsertUser" method which handles the "Save" button command. The method receives an object of type ViewModel which is already filled with the data from the controls "TextBoxName" and "TextBoxEMail".


public void InsertUser(ViewModel viewModel)
{
   
if (!this.ModelState.IsValid)
    {
       
return;
    }

   
BLManager blm = BLManager.Create();
    blm.AddUser(viewModel.Name, viewModel.EMail);


    this.Redirect("~/Default.aspx");
}


Once again, this code looks very much the same as when working under a common MVVM framework.


So, if you kept reading until this point then I hope you already agree that ASP.NET 4.5 supports MVVM mode of operation and the code is much simpler and cleaner than before.


My third and probably the most significant bullet for this post was that ASP.NET Web Forms 4.5 is much more testable. Is it?


Well, if you take a second look at the "InsertUser" method than you will probably agree that the method is 95% testable. The only problem is with the "Response.Redirect" code which under test environment will probably fail because the "Response" object does not exist.


Unfortunately, there is no out of the box support from Web Forms 4.5 to solve this issue. I can only guess that it will probably be supported in the future because it is quite a trivial task to implement. Let's try to do it our self:


public void InsertUser(ViewModel viewModel)
{
   
if (!this.ModelState.IsValid)
    {
       
return;
    }


    BLManager blm = BLManager.Create();
   
blm.AddUser(viewModel.Name, viewModel.EMail);


    this.Redirect("~/Default.aspx");
}


The "Redirect" method is an extension method which basically detects whether we are running under ASP.NET or under a testing environment. Take a look at the full source code. I stole some ideas from ASP.NET MVC's ActionResult mechanism.


Once we abstract the page action result we can focus on executing the page under a unit test. Our task is to write a test which verifies that the page rejects any request that has invalid user input (for example, empty user name).


The tricky part is filling the Page.ModelState with validation errors that would have been generated by an invalid request. Here is the code:


[TestClass]
public class PageCreateUserTest
{
    [
TestMethod]
    
public void VerifyPageDoesNotAllowCreationOfUserWithEmptyName()
    {
       
//
       
// Prepare a view + view model
       
//
       
PageCreateUser.ViewModel viewModel = new PageCreateUser.ViewModel();
       
PageCreateUser page = new PageCreateUser();


        //
       
// Create a container of values which represents the values 
        // that are submitted by the HTTP form
       
//
       
Dictionary<string,string> values =
           
new Dictionary<string, string>(){{"name", " "
}};


        //
       
// Prepare contextual objects which serve the binding process
       
//
       
DictionaryValueProvider<string> valueProvider =
                       
new DictionaryValueProvider<string>(values, null
);
       
ModelMetadata modelMetadata =
                       
ModelMetadataProviders.Current.GetMetadataForType(
                            ()=>viewModel,
                           
typeof(PageCreateUser.ViewModel
));
       
ModelBindingContext context = new ModelBindingContext();
       
context.ModelMetadata = modelMetadata;
       
context.Model = viewModel;
       
context.ValueProvider = valueProvider;

       
ModelBindingExecutionContext executionContext =
                         
new ModelBindingExecutionContext
()
                         
{
                             
ModelState = page.ModelState
                         
};


        //
       
// Look for a model binder for our ViewModel
       
//
       
IModelBinder binder = ModelBinders.Binders[
                                 
typeof(PageCreateUser.ViewModel
)];
       
if (binder == null)
       
{
           
binder =
ModelBinders.Binders.DefaultBinder;
       
}


        //
       
// Do the magic
       
// The binder is responsible for filling the ModelState of the page
       
//
       
binder.BindModel(executionContext, context);


        //
       
// Call the page and monitor the result
        
//
       
page.InsertUser(viewModel);


        //
       
// Verify that page rejected our request by not taking any action
       
//
       
ActionResult actionResult = ActionResultInvoker.LastExecutedResult;
       
if (actionResult != null)
       
{
           
Assert.Fail("Unexpected action was taken by UserCreate.aspx: " +
                            actionResult.GetType().FullName);
       
}
   
}
}


Full source code can be found here 


What do you think? Can we say that ASP.NET WebForms 4.5 is a testable framework? If so, then what do we need ASP.NET MVC for? Am I going to far?


 

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

2 comments

  1. manthan gogari30 ביולי 2012 ב 8:49

    is it possible to use this MVVM implementation in addition to ASP.NET Dynamic Data Web sites that allows creating of Templates for rendering of the Views. http://www.asp.net/web-forms/videos/aspnet-dynamic-data/your-first-scaffold-and-what-is-dynamic-data. I am trying to implement MVVM pattern in ASP.NET but also need to have templating ability of Dynamic Data applications.

    Any pointers how to do that would be great.

    Reply
    1. Panos Roditakis2 בינואר 2015 ב 0:40

      Mvvm is possible in WebForms. Use a FormView with ViewState disabled, default mode to edit, set EditTemplate programmatically using LoadTemplate on page Init, assign its SelectMethod to a one that returns the ViewModel, assign its UpdateMethod to a one that calls TryUpdateModel, on page Preload call FormView's UpdateItem(false), work with ViewModel on rest of page lifecycle and put a flag on viewmodel to rebind FormView on Prerender to apply any changes in between. With basic controls, this works like a charm. Having 3rd party controls that support nullable or null values would also help with model binding.
      Regards.

      Reply