December 2009 - Posts

For last few days I was pushing Brad Wilson to add support for multiple metadata provider in the ASP.NET MVC framework. The reason is very simple, lets say you are developing a relatively large application which has multiple modules, new modules can be plugged-in in future, you do not want all the module developers to use the same ModelMetadataProvider, instead each module developer can pick the provider whatever is appropriate for that module, it can be the built-in DataAnnotations provider, the Fluent Metadata provider or any custom xml/database driven provider. Currently, there is no out of the box support for multiple model metadata providers in the ASP.NET MVC framework. But good news is, as discussed in the above mentioned thread , the MVC Framework is extensible enough to add such support.

As a user of System.Web.Mvc.Extensibility you do not have to do any thing special, you can use both the Fluent version or the built-in DataAnnotations provider the way you using it now. If the provider does not exists for a given model, it will automatically fallback to the default DataAnnotations provider, if the same model is registered in multiple provider the last registered provider will be given precedence similar to the most of the IoC containers. For example, in the same application, I am using the DataAnnotations provider for the ProductDisplayModel and Fluent Provider for the ProductEdtiModel like the following:

public class ProductDisplayModel
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }

    public string Name { get; set; }

    [DisplayName("Category")]
    public string CategoryName { get; set; }

    [DisplayName("Supplier")]
    public string SupplierName { get; set; }

    [DataType(DataType.Currency)]
    public decimal Price { get; set; }
}

public class ProductEditModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    public Category Category { get; set; }

    public Supplier Supplier { get; set; }

    public decimal Price { get; set; }
}

public class ProductEditModelConfiguration : ModelMetadataConfigurationBase<ProductEditModel>
{
    public ProductEditModelConfiguration()
    {
        Configure(model => model.Id).Hide();

        Configure(model => model.Name).Required("Name cannot be blank.")
                                      .MaximumLength(64, "Name cannot be more than 64 characters.");

        Configure(model => model.Category).DisplayName("Category")
                                          .Required("Category must be selected.")
                                          .AsDropDownList("categories", "[Select category]");

        Configure(model => model.Supplier).DisplayName("Supplier")
                                          .Required("Supplier must be selected.")
                                          .AsDropDownList("suppliers", "[Select supplier]");

        Configure(model => model.Price).FormatAsCurrency()
                                       .Required("Price cannot be blank.")
                                       .Range(10.00m, 1000.00m, "Price must be between 10.00-1000.00.");
    }
}

And the exactly the same code for rendering the model:

<h2>Details</h2>
<fieldset>
    <legend>Fields</legend>
    <%= Html.DisplayForModel()%>
</fieldset>
<p>
    <%= Html.ActionLink("Edit", "Edit", new { id = Model.Id }) %> |
    <%= Html.ActionLink("Delete", "Delete", new { id = Model.Id })%> |
    <%= Html.ActionLink("Back to List", "Index") %>
</p>

 

<h2>Edit</h2>
<% Html.EnableClientValidation(); %>
<% using (Html.BeginForm()) {%>
    <fieldset>
        <legend>Fields</legend>
        <%= Html.EditorForModel() %>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
<% } %>
<div>
    <%= Html.ActionLink("Back to List", "Index") %>
</div>

And it will appear as the following:

DisplayFor

EditorFor

If you want to create a new provider which will plug-in into this multi provider scenario, all you have to do create a class which inherits from the ExtendedModelMetadataProviderBase and override the two abstract methods.

Enjoy!!!

Download: github

Shout it

In my previous post, I mentioned that Area is one of the thing that the coming ASP.NET MVC2 needs heavy enhancements (I also created an Issue in CodePlex). The idea was, Area should have dynamic registering/unregistering support instead of registering all the area at the application start. Recently, I was playing with the Area and trying to find out what are the issues (specially road blocks) for adding such support and it turns out the issue is rather with the ASP.NET Routing instead of the ASP.NET MVC Framework itself.

Before getting down to the main discussion, lets us create a very basic area supported ASP.NET MVC application, lets assume that we have an application which has Blog and Forum as Area and our main Index view has the following code:

<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<ul>
    <li><%= Html.ActionLink("Blog", "Index", "Home", new { area = "Blog" }, null)%></li>
    <li><%= Html.ActionLink("Forum", "Index", "Home", new { area = "Forum" }, null)%></li>
</ul>

Nothing special, just two links which will take you the individual area home. In our global.asax we have the following code:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}

If you run the application and click the individual link, it will take you to the corresponding area home as expected. Now, change the code to the following:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    AreaRegistration.RegisterAllAreas();
}

We just swapped the lines, now we are registering the application routes first and then the areas. If you run the application and click any of the links, it will throw “Resource Not Found”. If you recall when the Routing was first introduced, you know that ordering of Route registration is very important and this is exactly what happens here, when we are registering the application route first, the ASP.NET Routing treats the Area Name as Controller Name.

So, if we want to add dynamic area registration we have to make sure that the Routes of Area is registered prior the application routes, so I checked the MapRoute of AreaRegistratonContext if I can make an extension method which inserts the Area Routes at the top instead of appending it at the bottom, we can easily add the support for dynamic area registration. The MapRoute of AreaRegistratonContext internally uses the extension method MapRoute of RouteCollection. The following is the MapRoute extension method:

public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) {
    if (routes == null) {
        throw new ArgumentNullException("routes");
    }
    if (url == null) {
        throw new ArgumentNullException("url");
    }

    Route route = new Route(url, new MvcRouteHandler()) {
        Defaults = new RouteValueDictionary(defaults),
        Constraints = new RouteValueDictionary(constraints),
        DataTokens = new RouteValueDictionary()
    };

    if ((namespaces != null) && (namespaces.Length > 0)) {
        route.DataTokens["Namespaces"] = namespaces;
    }

    routes.Add(name, route);

    return route;
}

Check the line 19 which is using the Add with the route name and object. My initial assumption was, since the RouteCollection is Collection type it should have the support for Insert with the name as like as the Add, but it turns out that I am wrong, the RouteCollection does have an Insert but it does not take name as argument and there is no other way to put the name against the route in the RouteCollection. After spending some time with Reflector I find the only important place that it uses the route name is in the non virtual GetVirtualPath method. So if we use the Insert without the name, I think the url generation which depends on the route name will not work. My next try was, what if I created a inherited version of RouteCollection and assign it to the Routes of RouteTable but it does not have any setter, so no luck and could not find any other way to do any kind of monkey patching.

So my request to the ASP.NET Team to add an overloaded version of Insert to RouteCollection which has name as argument in .NET 4.0 and the next service pack for 3.5.

Download : MVCModuleTest.zip

Shout it

In my last post, one of the thing you complained about maintaining view model and its meta data in two separate place and I admit this becomes a pain when you browse through the codes. I have changed the  implementation, so now you will be able to configure the meta data very much like the way you do in Fluent NHibernate or Entity Framework 4.0 Code only version (thanks to Mohamed Meligy from brining it up). There has been also two additional  features that I have added, built-in support to show DropDownList/Select element and implicitly adding Email/Url regular expression validation when you apply those in string property.

The following shows both the DataAnnotations and the Fluent version:

DataAnnotations:

public class ProductEditModel
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }

    [Required(ErrorMessage = "Name cannot be blank.")]
    [StringLength(64, ErrorMessage = "Name cannot be more than 64 characters.")]
    public string Name { get; set; }

    [DisplayName("Category")]
    [Required(ErrorMessage = "Category must be selected.")]
    public Category Category { get; set; }

    [DisplayName("Supplier")]
    [Required(ErrorMessage = "Supplier must be selected.")]
    public Supplier Supplier { get; set; }

    [Required(ErrorMessage = "Price cannot be blank.")]
    [Range(10, 1000, ErrorMessage = "Price must be between 10.00-1000.00.")]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }
}

Fluent:

public class ProductEditModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    public Category Category { get; set; }

    public Supplier Supplier { get; set; }

    public decimal Price { get; set; }
}

public class ProductEditModelConfiguration : ModelMetadataConfigurationBase<ProductEditModel>
{
    public ProductEditModelConfiguration()
    {
        Configure(model => model.Id).Hide();
        Configure(model => model.Name).Required("Name cannot be blank.").MaximumLength(64, "Name cannot be more than 64 characters.");
        Configure(model => model.Category).DisplayName("Category").Required("Category must be selected.").DropDownList("categories", "[Select category]");
        Configure(model => model.Supplier).DisplayName("Supplier").Required("Supplier must be selected.").DropDownList("suppliers", "[Select supplier]");
        Configure(model => model.Price).AsCurrency().Required("Price cannot be blank.").Range(10, 1000, "Price must be between 10.00-1000.00.");
    }
}

Nothing special, all you have to do is create a class that inherits from ModelMetadataConfigurationBase and pass your view model class as generic definition, then configure the properties. And you no longer have to register it with the BootstrapperTaskBase like you did before, the System.Web.Mvc.Extensibility is intelligent enough to auto register those.

What do you think, again comments and suggestion are greatly appreciated.

Download: github

Shout it

I have just included a Fluent Metadata provider in my open source System.Web.Mvc.Extensibility project. Currently it contains all of the features of the Built-in DataAnnotations Metadata provider that comes with the ASP.NET MVC 2 framework. Consider it as a holiday special from me for the ASP.NET MVC community :-).

The main reason I am not fond of the DataAnnotations provider is, it is completely Attribute based, In my opinion, attributes adds extra noise to the code which I always tried to avoid, I love pure POCO. Beside the noise, the most important drawback of using attribute is that there is no conventional way to inject your dependencies into your attribute by your preferred IoC container and DataAnnotations is no exception in this case. Another issue that is also true for the attribute based model is that you are always free to decorate your code with invalid attributes and there is no compile type checking, For example, in this case nothing going to stop you to add StringLength or data type DateTime attribute to a integer based property.

These are the main driving force to come up with my Fluent Medata provider.

Now, lets take a quick look how to define the metadata for the model with the fluent metadata provider, but, first lets see the DataAnnotations attributed based model of the sample application:

public class ProductDisplayModel
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }

    public string Name { get; set; }

    public string CategoryName { get; set; }

    public string SupplierName { get; set; }

    [DataType(DataType.Currency)]
    public decimal Price { get; set; }
}

public class ProductEditModel
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }

    [Required(ErrorMessage = "Name cannot be blank.")]
    [StringLength(64, ErrorMessage = "Name cannot be more than 64 characters.")]
    public string Name { get; set; }

    [DisplayName("Category")]
    [Required(ErrorMessage = "Category must be selected.")]
    public Category Category { get; set; }

    [DisplayName("Supplier")]
    [Required(ErrorMessage = "Supplier must be selected.")]
    public Supplier Supplier { get; set; }

    [Required(ErrorMessage = "Price cannot be blank.")]
    [Range(10, 1000, ErrorMessage = "Price must be between 10.00-1000.00.")]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [ScaffoldColumn(false)]
    public SelectList Categories { get; set; }

    [ScaffoldColumn(false)]
    public SelectList Suppliers { get; set; }
}

For the fluent version, you first have to create a class which inherits from the BootstrapperTaskBase so that the System.Web.Mvc.Extensibility framework can automatically register it when the application starts, the following is the complete code which would work exactly same as the above:

public class RegisterModelMetadata : BootstrapperTaskBase
{
    protected override void ExecuteCore(IServiceLocator serviceLocator)
    {
        IModelMetadataRegistry registry = serviceLocator.GetInstance<IModelMetadataRegistry>();

        registry.Register<ProductDisplayModel>(configurator =>
                                               {
                                                   configurator.Configure(model => model.Id).Hide();
                                                   configurator.Configure(model => model.Price).AsCurrency();
                                               })
                .Register<ProductEditModel>(configurator =>
                                               {
                                                   configurator.Configure(model => model.Id).Hide();
                                                   configurator.Configure(model => model.Name).Required("Name cannot be blank.").MaximumLength(64, "Name cannot be more than 64 characters.");
                                                   configurator.Configure(model => model.Category).DisplayName("Category").Required("Category must be selected.");
                                                   configurator.Configure(model => model.Supplier).DisplayName("Supplier").Required("Supplier must be selected.");
                                                   configurator.Configure(model => model.Price).AsCurrency().Required("Price cannot be blank.").Range(10, 1000, "Price must be between 10.00-1000.00.");
                                                   configurator.Configure(model => model.Categories).Hide();
                                                   configurator.Configure(model => model.Suppliers).Hide();
                                               });

        ModelMetadataProviders.Current = new ExtendedModelMetadataProvider(registry);
    }
}

You can find the complete example in the sample folder.

When configuring the  model you will find that it gives you the options that are only applicable for that data type, for example, you cannot apply string length constraint to an integer property, currency is only valid for the decimal type etc etc. It also has a nice extensibility model to add more option which I will cover in the future blog post.

What do you think? Comments are suggestions are greatly appreciated.

Download: github

Shout it

As you already know ASP.NET MVC 2 hits the RC, as far as I know this is the last release before the final. There has been quite a few things that the ASP.NET MVC team has added since the v1.0. I know people says human nature is “More you get the more you want”, but the requirements that I am presenting here is not new, instead these are the enhancements of the newly added features and without it these features does not seem complete.

Client Validation

Yes, there has been one improvement since the beta, currently the validation script can be placed at the bottom of the page (which is recommended), but it is sill generating inline scripts after the form tag. My suggestion was to the ASP.NET MVC team was to maintain an internal queue of validation scripts and expose a method (like HtmlHelper.WirtieValidationRules() ) so that the we can call it wherever we think is appropriate. And I think the Html.EnableClientValidation() is also a redundant call, The framework can check the browser capability whether it supports javascript or not. Yes you can say, what if I do not want the javascript validations at all, I think in that case there should be switch in either web.config or a in a static validation setting class where we apply it globally. The reason I dislike this method is that I think, there is a very good chance that I will forget to call that method entirely or I may call it inside the Html.BeginForm() where it does not work at all.

The next in client side validation is integration with jQuery validation libraray, As you know that the jQuery Validation Library is now a part of the Default ASP.NET MVC project, but the jQuery adapter script which is required to propagate the server side validation rules in client side still not in the default ASP.NET MVC project, The default ASP.NET MVC project only contains the MS Ajax Library adapter. I am not sure without that adapter script including the jQuery Validation script in the default ASP.NET MVC project makes any sense. I think the jQuery validation adapter script is still in the future project, which means it is not officially supported. I would expect, it will be included in the default ASP.NET MVC project when the RTM is out.

Templates

Template aka DisplayFor/EditorFor is a nice addition, but there are currently few things that I would like to discuss, the first and less important one, there is no way to create code templates like the built-in templates, as per Brad Wilson they are evaluating it for v3. The second and more important one, currently the built-in ModelMetaData (DataAnnotation) provider only cares the things that it knows, which means if I decorate my Model with some custom attributes, it is not possible to access those attributes in from the template. The only way to resolve this issue is to write a custom provider. But I think writing a provider for performing a simple task like showing a DropDownList is an overkill. The last one is DataAnnotation which is rather a feature request. One of the important attribute of DataAnnotation is DataTypeAttribute which takes the data type as an enumeration. I think the ASP.NET MVC framework can make more intelligent decisions when this attribute is applied currently it only uses it for format string . For example, when I mark a property of a Model as Email or Url it should automatically add the corresponding validators in the ModelMetaData. There are also few small issues that I think is the limitation of the DataAnnotation itself rather than the ASP.NET MVC. For example, both Html and MultilineText cannot be applied to the same model property, StringLength attribute only support max length but no min length, still some of the important validators are missing such as data type checking, comparer, range for other than numeric etc which makes it as not as powerful as Webforms validation.

Area

I think this is the most weakest thing in the MVC2. Currently it is implemented in a way that you either get all or none. My initial assumption was it supports pluggable areas out of the box. But there is no built-in support for enabling/disabling/loading/unloading Areas. The last feedback I got from the MVC team is, they have this as internal work item and they are not sure when it will be added in the core. For the time being, my suggestion would be to change the AreaRegistration, so that it can at least support individual area registration rather than bulk.

I will be looking forward to the ASP.NET MVC Team to include the above enhancements in the RTM.

Shout it

As mentioned in my last post’s comment, Javier has posted his thoughts. Though I criticized his pet project in three different aspects, but in his response he has only discussed the first two, and skipped the last one completely. Please read his response first before continuing this post.

So, once again what is the correct way of implementing it.

I don’t think the documentation could do any help as I understand the ins and outs of the MVCTurbine codebase. The first thing that I think needs to be corrected as I mentioned in my last post not to come up with a Generic API for the all the IoC container’s service registration and this is one of the things that needs to be decided at the early stage of the development and the rest of the implementation will depend upon it. I am sure the initiators of Common Service Locator did face the same situation and they decided not to handle the registration part which I think was the correct and wise decision.

One of the goal of both MvcTurbine and my new extension is not to clutter your global.asax, instead it should have individual Blade(in MvcTurbine) or BootstrapperTask (which I first introduced in my another pet project KiGG) which would perform individual tasks. But, due to above mentioned limitation of MvcTurbine, you have to use the global.asax when setting up container in real life applications (Check the last example of Javier’s post). This is not a big issue for a tiny mvc application, but if you are developing a relatively large application with multi-module system, it is a show stopper as there is no way the individual module can register its isolated services in the container with the native features of that container.

At the end, due to those limitations of MvcTurbine I came up with my own version. So I would suggest you check both the projects and pick whatever you think suites your need.

[Edit: The intension of this post not to criticized Javier Lozano personally or his technical abilities, I do respect and love to work with him, at the end we are both trying to enrich the asp.net mvc ecosystem]

[Edit2: I got few feedbacks that my tone was bit arrogant and I am extremely sorry for that, I hope now it sounds reasonable.]

Shout it

After hearing Phil Haack and Scott Hanselman in their latest podcast and PDC session I decided to give a good look at the MvcTurbine project in this weekend. I totally agree the with intend of this project, let your favorite IoC to rule everywhere, but I do not think it has been implemented in the correct way. There are three important design decisions that the MvcTurbine team should re-evaluate:

Generic Component Registration API

Rather than allowing you to use your preferred IoC registration features, it prefers to use its own generic syntax for component registration, which is simply wrong. In real application, we need more support from our IoC such as lifetime management, auto wiring, modularity like (Ninject/Autfac Modules, StructureMap Registry) etc. In case of this generic API we have to throw away the features of these IoCs.

Missing Common Service Locator

I think the Common Service Locator(CSL) has become the standard to abstract the underlying IoC from your application code and all the popular IoCs in .NET world has an adapter for CSL. But rather than using/extending it, it has its own version of service locator.

Limited support for Action Filter injection

It uses a host kinda Attribute to inject the dependencies into the actual action filter attribute and this host is not capable of passing the attribute specific values to the actual attribute and even if this feature is implemented in the future it will be based upon the string based property name which would work on top of reflection.

Now, lets see how my  new extension solves the above issues and how easily you can start plugging it in your application. First lets take a quick look of the project structure of this extension.

SWMEStructure

As you can see other than the Core, each IoC has its own implementation. Currently it has the support for Autofac, Ninject, StructureMap, Unity and Windsor. When plugging it in your application, it requires five simple steps:

  1. Add reference of the Core Project.
  2. Add reference of you prefered IoC project and binaries of that IoC.
  3. Change the Global.asax so that it inherits from your prefered IoC supported HttpApplication.
  4. Create a class that inherits from RegisterRoutesBase and register your routes over there.
  5. Create a class as per your IoC that allows component registration. For example, both Autofac and Ninject supports Module for component registration and StructureMap uses Registry. Since both Unity and Windsor does not have modules support there is an interface IModule which you have to implement for your application component registration (Check the sample application for the complete reference).

And that’s it, you are all set to go.

Now, lets see the above steps in more detail, I will use the Autofac in this case. But you can check the other examples if your prefered IoC is other than Autofac.

First, lets add the required references, once your are done, your Mvc application will have the following references:

SWMEAutofacStructure

Next, change the global.asax so that it inherits from the Autofac supported HttpApplication like the following:

namespace Demo.Web.Autofac
{
    using System.Web.Mvc.Extensibility.Autofac;

    public class MvcApplication : AutofacMvcApplication
    {
    }
}

Next, create a class to register the routes:

public class RegisterRoutes : RegisterRoutesBase
{
    public RegisterRoutes(RouteCollection routes) : base(routes)
    {
    }

    protected override void ExecuteCore(IServiceLocator locator)
    {
        Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        Routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Product", action = "Index", id = string.Empty } );
    }
}

And at last an Autofac module to register your application specific components:

public class RegisterServices : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.Register<InMemoryDatabasae>().As<IDatabase>();
        builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
    }
}

If you browse the sample applications, you will find there are quite a few things that is going under the hood. Lets take a quick look how the Product Controller is implemented:

public class ProductController : Controller
{
    private readonly IRepository<Product> repository;

    public ProductController(IRepository<Product> repository)
    {
        this.repository = repository;
    }

    public ActionResult Index()
    {
        return View(repository.All().Select(product => product.AsDisplayModel()));
    }

    public ActionResult Details(int id)
    {
        return View(repository.Get(id).AsDisplayModel());
    }

    public ActionResult Create()
    {
        return View(new ProductEditModel());
    }

    [HttpPost]
    public ActionResult Create(FormCollection form)
    {
        ProductEditModel model = new ProductEditModel();

        if (TryUpdateModel(model, form.ToValueProvider()))
        {
            Product product = model.AsProduct();
            product.Id = repository.All().LastOrDefault().Id + 1;

            repository.Add(product);

            return RedirectToAction("Index");
        }

        return View(model);
    }

    public ActionResult Edit(int id)
    {
        return View(repository.Get(id).AsEditModel());
    }

    [HttpPost]
    public ActionResult Edit(ProductEditModel model)
    {
        if (ModelState.IsValid)
        {
            Product product = model.AsProduct();

            repository.Update(product);

            return RedirectToAction("Index");
        }

        return View(model);
    }

    public ActionResult Delete(int id)
    {
        return View(repository.Get(id).AsDisplayModel());
    }

    [HttpPost]
    public ActionResult Delete(int id, string confirm)
    {
        repository.Delete(id);

        return RedirectToAction("Index");
    }
}

As you can see the the controller requires the repository which we have registered in the RegisterServices module, but the controller, action filters (Not decorated in the controller) and model binders are automatically registered by this extension.

Lets take a look at the Model Binder, in the sample application we have only one Model Binder which is used to create the product edit view model. Under the hood only the category id and supplier Id is passed and it uses the corresponding repositories to populate the Category and Supplier property of this view model.

[BindingTypes(typeof(ProductEditModel))]
public class ProductEditModelBinder : DefaultModelBinder
{
    private readonly IRepository<Category> categoryRepository;
    private readonly IRepository<Supplier> supplierRepository;

    public ProductEditModelBinder(IRepository<Category> categoryRepository, IRepository<Supplier> supplierRepository)
    {
        this.categoryRepository = categoryRepository;
        this.supplierRepository = supplierRepository;
    }

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        const string CategoryProperty = "Category";
        const string SupplierProperty = "Supplier";

        if (propertyDescriptor.Name.Equals(CategoryProperty, StringComparison.OrdinalIgnoreCase))
        {
            return GetValue(CategoryProperty, categoryRepository, controllerContext, bindingContext);
        }

        if (propertyDescriptor.Name.Equals(SupplierProperty, StringComparison.OrdinalIgnoreCase))
        {
            return GetValue(SupplierProperty, supplierRepository, controllerContext, bindingContext);
        }

        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }

    private static object GetValue<TEntity>(string propertyName, IRepository<TEntity> repository, ControllerContext controllerContext, ModelBindingContext bindingContext) where TEntity : EntityBase
    {
        ValueProviderResult result = bindingContext.ValueProvider.GetValue(controllerContext, propertyName);
        int? id = (result != null) ? (int?) result.ConvertTo(typeof(int?)) : null;

        return (id.HasValue && id.Value > 0) ? repository.Get(id.Value) : default(TEntity);
    }
}

The model binder inherits from the default model binder so that we can advantages of built-in features, the only exception is the Category and Supplier property where we are using the repository to retrieve it from our data store. One important thing I think you noticed that the Model Binder is decorated with an special attribute BindingTypes, this attribute indicates for which type(s) the binder will be used. This extension uses this attribute to register the model binders in the Model Binder collection.

And at last the Filters, the following is the code of PopulateCategoriesAttriute action filter:

public sealed class PopulateCategoriesAttribute : PopulateModelAttribute
{
    private readonly IRepository<Category> repository;

    public PopulateCategoriesAttribute(IRepository<Category> repository)
    {
        this.repository = repository;
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ProductEditModel editModel = filterContext.Controller.ViewData.Model as ProductEditModel;

        if (editModel != null)
        {
            editModel.Categories = new SelectList(repository.All(), "Id", "Name", editModel.Category);
        }
    }
}

As we can see that the Filter accept a repository in the constructor and it has not been decorated in the Controller methods, if you are wondering then how this filter come into action, let me explain a bit. When Oxite was initially released a lot people criticized it, but in next (current) version the Oxite came up with lots of excellent ideas. The Filter registration that I about to show you is taken from the Oxite with a slight change in the implementation. Although, this extension support filter property injection, but I would suggest to use the following fluent registration. To apply filter in controller, you first have to create a class which inherits from RegisterFiltersBase class. The following shows how the PopulateCategoriesAttribute and PopulateSuppliersAttribute are applied in the product controller of the sample application.

public class RegisterFilters : RegisterFiltersBase
{
    public RegisterFilters(IFilterRegistry filterRegistry) : base(filterRegistry)
    {
    }

    protected override void ExecuteCore(IServiceLocator serviceLocator)
    {
        FilterRegistry.Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Create())
                      .Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Create(null))
                      .Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Edit(0))
                      .Register<ProductController, PopulateCategoriesAttribute, PopulateSuppliersAttribute>(c => c.Edit(null));
    }
}

There are quite a few overloads for the Filter registration, if you want to apply filters to the whole controller, just skip the action part.

And that’s it for today.

I also think this would be nice addition for the MVCContrib project. So please download the code and play with it and do let me know your feedbacks.

Source Code: github

 

Shout it
More Posts