Nov 12, 2009

ResourceManager notes

The benefit to ResourceManager is that it can smartly load differently resource based on the current UI culture. ResourceManager basically it is used to read embedded resource or file based resource. But it is extensible. It has three constructor, and one static factory method

public ResourceManager(String baseName, Assembly assembly) 
public ResourceManager(Type resourceSource);


public ResourceManager(String baseName, Assembly assembly, Type usingResourceSet);
public static ResourceManager CreateFileBasedResourceManager(String baseName, String resourceDir, Type usingResourceSet) 

The most commonly used constructor is the first one. It try to read the embedded resource in the assembly, and the baseName is used to search inside of assembly. The second constructor basically does the same thing of first one. The type of resourceSource is the strongly type resource that is automatically generated by tool. This type has some static method to wrap the resource. Here is used to calcuated the base name, and assembly. Both of these two method use default RuntimeResourceSet to cache the resource. The third one allowed you specify the type of ResourceSet to cached the resource. This is useful if you have resource in different format. For example, if you baseName is "base", and dll name is "asm", the Resource manager will search in that dll for a resource named "asm.base.resources". By default it will use RuntimeResourceSet to cache the resource. But RuntimeResourceSet is internal, so that you can not use that. Following is clone of RuntimeResourceSet.

public RuntimeResourceSetClone(IResourceReader reader) : base(reader) { }

public RuntimeResourceSetClone(string fileName)
{
    ResourceReader reader = new ResourceReader(fileName);
    this.Reader = reader;
}

public RuntimeResourceSetClone(Stream stream)
{
    ResourceReader reader = new ResourceReader(stream);
    this.Reader = reader;
}

//we can use this ResourceSet to cache the default resource format
ResourceManager manager = new ResourceManager("UI", Assembly.GetExecutingAssembly(), typeof(RuntimeResourceSetClone));
Console.WriteLine(manager.GetString("Name"));

When you use resx file to manage resources, the compiler does not actually create resx format resource , and embed it in the assembly. What it does is to generate resource in defautlt resource format. Say you have UI.resx file, and the embedded resource name will be assemblyName.ui.resources. In case, you really want to use it in resources, you have to rename the extension to resources. The following shows how to read that. Please note the you need put the dll name with file name together.

//the loose file name is filename.resources
//the dll name is dllname
ResourceManager manager = new ResourceManager("dllname.filename", Assembly.GetExecutingAssembly(), typeof(ResXResourceSet));
Console.WriteLine(manager.GetString("Type"));

For embedded file resource, the neutral resource will be embedded in the main dll, for other resource, an assembly will be put in sub folder in the name of the culture (eg. zh-CN), the name of assembly is like mainAssemblyName.resources.dll.

The static factory method is used to read loose file in file system. One of the benefit using loose file is that saving memory, because resource is not loaded in the memory. All your resources file need to follow format, baseName.cultureName.resources . Yes, the extension has to be resources, even thought the content is not in resx format. Below is some example.

//if the resource format is default resource format, set usingResourceSet type to null,
var man = ResourceManager.CreateFileBasedResourceManager("baseName", "folderName", null);
Console.WriteLine(man.GetString("Name"));

//if resource file is in resx format, (the extension need to be ressources)
var man = ResourceManager.CreateFileBasedResourceManager("baseName", "folderName", typeof(ResXResourceSet));

The algorithm of ResourceManager depends on the UICulture heavily. First it locate the resource for current culture, if found return the value, if not fall back, to neutral culture, then invariant culture. For example, if current culture is zh-CN, it will try in the path of zh-CN, zh-CHS, Invariant. So to implement that, resource manager holds a ResourceSet for each availabe culture, depending how much culture the resources support. For each ResourceSet, it holds a Dictionary object to cache to resource. The dictionary is filled by IResourceReader in the consturctor of ResourceSet. After that resource will be disconnected because all resource is cached. The default RuntimeResourceSet is optimized, so it does not use the dictionary.

To extend ResourceManager, we just need implement how the ResourceSet is created, and how the resource is read by IResourceReader. The default ResourceSet interface is designed for file based resource.

public ResourceSet(Stream stream) 
public ResourceSet(IResourceReader reader) 
public ResourceSet(String fileName)

Because of ResourceManager will use the same interface to create ResourceSet, if your solution is also file based, you just need a ResoursSet follow this constructor pattern. And you don't need to create a new ResourceManager, that is how Resx file is implemented. But if your solution is not file based, you need to SubClass your ResourceManager as well. ResourceSet will use IResourceReader to build its dictionary. IResourceReader is actually very simple Another drawback of ResourceManager is lack of creating or updating resource.

public interface IResourceReader : IEnumerable, IDisposable
{
        void Close();
        IDictionaryEnumerator GetEnumerator();
}

CurrentCulture can not be set to Neutural Culture

var c = CultureInfo.GetCultureInfo("en");
Console.WriteLine(c.Name); //en
var c2 = CultureInfo.CreateSpecificCulture("en");
Console.WriteLine(c2); //en-US
Thread.CurrentThread.CurrentCulture = c2; //ok
//following with throw
//System.NotSupportedException: Culture 'en' is a neutral culture.
Thread.CurrentThread.CurrentCulture = c; 

Because of this, the following code is necessary

 if (Request.UserLanguages != null &&
        Request.UserLanguages.GetLength(0) > 0)
    {
        CultureInfo cultureInfo =
            new CultureInfo(Request.UserLanguages[0]);
        Thread.CurrentThread.CurrentUICulture = cultureInfo;
        Thread.CurrentThread.CurrentCulture =
            CultureInfo.CreateSpecificCulture(cultureInfo.Name);
    }

This code gets the user's language preference from the first element of the Request.UserLanguages array. If the user's settings are such that, Request.UserLanguages[0] will be "en-US". We create a new CultureInfo object and assign it to CurrentUICulture. The next line uses CultureInfo.CreateSpecificCulture to create a specific culture from a potentially neutral culture. In this example the culture will be no different (i.e., it will be "en-US"), but if the original language preference was "en", the specific culture would be "en-US". Recall that this step is necessary because CurrentCulture must be culture-specific. Later in this chapter, we implement a more sophisticated version of this first attempt that iterates through each of the language preferences looking for a language that matches the application's supported languages.

Nov 11, 2009

How Routing Works in asp.net mvc

The mvc stack start with the UrlRoutingModule. The module handle two HttpApplication event.

/* the order of application event is:
OnBeginRequest
OnAuthenticateRequest
OnPostAuthenticateRequest
OnAuthorizeRequest
OnPostAuthorizeRequest
OnResolveRequestCache
OnPostResolveRequestCache --> handled by  PostResolveRequestCache
OnPostMapRequestHandler --> handled by PostMapRequestHandler
OnAcquireRequestState
OnPostAcquireRequestState
OnPreRequestHandlerExecute
Page_Load Event of the Page
OnPostRequestHandlerExecute
OnReleaseRequestState
OnPostReleaseRequestState
OnUpdateRequestCache
OnPostUpdateRequestCache
OnEndRequest
OnPreSendRequestHeaders */
public virtual void PostResolveRequestCache(HttpContextBase context) { 
    // Match the incoming URL against the route table
    RouteData routeData = RouteCollection.GetRouteData(context); 

    // Do nothing if no route found
    if (routeData == null) { 
        return;
    }

    // If a route was found, get an IHttpHandler from the route's RouteHandler 
    IRouteHandler routeHandler = routeData.RouteHandler;
    if (routeHandler == null) { 
        throw new InvalidOperationException( 
            String.Format(
                CultureInfo.CurrentUICulture, 
                RoutingResources.UrlRoutingModule_NoRouteHandler));
    }

    // This is a special IRouteHandler that tells the routing module to stop processing 
    // routes and to let the fallback handler handle the request.
    if (routeHandler is StopRoutingHandler) { 
        return; 
    }

    RequestContext requestContext = new RequestContext(context, routeData);

    IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
    if (httpHandler == null) { 
        throw new InvalidOperationException(
            String.Format( 
                CultureInfo.CurrentUICulture, 
                RoutingResources.UrlRoutingModule_NoHttpHandler,
                routeHandler.GetType())); 
    }

    // Save data to be used later in pipeline
    context.Items[_requestDataKey] = new RequestData() { 
        OriginalPath = context.Request.Path,
        HttpHandler = httpHandler 
    }; 

    // Rewrite path to something registered as a managed handler in IIS.  This is necessary so IIS7 will 
    // execute our managed handler (instead of say the static file handler).
    context.RewritePath("~/UrlRouting.axd");
}

public virtual void PostMapRequestHandler(HttpContextBase context)
{
    RequestData requestData = (RequestData)context.Items[_requestDataKey];
    if (requestData != null)
    {
        // Rewrite the path back to its original value, so the request handler only sees the original path.
        context.RewritePath(requestData.OriginalPath);

        // Set Context.Handler to the IHttpHandler determined earlier in the pipeline.
        context.Handler = requestData.HttpHandler;
    }
}

The route matching is handled by "RouteData routeData = RouteCollection.GetRouteData(context);". It search the Routes defined in start up event. If RouteData is not found, the handling will be passed back. Below is the code how RouteCollection handle the matching.

//RouteCollection member
public RouteData GetRouteData(HttpContextBase httpContext) { 
//... code skip ...
    // Go through all the configured routes and find the first one that returns a match
    using (GetReadLock()) { 
        foreach (RouteBase route in this) {
            RouteData routeData = route.GetRouteData(httpContext);
            if (routeData != null) {
                //the first matching wins
                return routeData; 
            }
        } 
    } 
    return null; 
}

//Route member
public override RouteData GetRouteData(HttpContextBase httpContext) {
    // Parse incoming URL (we trim off the first two chars since they're always "~/")
    string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

    RouteValueDictionary values = _parsedRoute.Match(requestPath, Defaults); 

    if (values == null) {
        // If we got back a null value set, that means the URL did not match 
        return null;
    }

    RouteData routeData = new RouteData(this, RouteHandler); 

    // Validate the values 
    if (!ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { 
        return null;
    } 

    // Copy the matched values
    foreach (var value in values) {
        routeData.Values.Add(value.Key, value.Value); 
    }

    // Copy the DataTokens from the Route to the RouteData 
    if (DataTokens != null) {
        foreach (var prop in DataTokens) { 
            routeData.DataTokens[prop.Key] = prop.Value;
        }
    }

    return routeData;
} 

public string Url {
    get { 
        return _url ?? String.Empty; 
    }
    set { 
        // The parser will throw for invalid routes. We don't have to worry
        // about _parsedRoute getting out of sync with _url since the latter
        // won't get set unless we can parse the route.
        _parsedRoute = RouteParser.Parse(value); 

        // If we passed the parsing stage, save the original URL value 
        _url = value; 
    }
} 

When the Url is set for a route, a _parsedRoute is created based on the Url, the _parseRoute is used to match the request url with the help of default values. Then the constraint is applied. If match is found, the RouteData can be returned. Before that DataTokens is copied to RouteData as well. DataToken is not actually used in the process of matching, we can think of DataToken is some predefined RouteData. RouteCollection has another use , to generate url based on routes. When search RouteData, constraints are also tested. Normally, it is an regular expression, but you can roll out your customized constraints, here is an article about this.

//route collection member
public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
    requestContext = GetRequestContext(requestContext); 

    // Go through all the configured routes and find the first one that returns a match 
    using (GetReadLock()) { 
        foreach (RouteBase route in this) {
            VirtualPathData vpd = route.GetVirtualPath(requestContext, values); 
            if (vpd != null) {
                vpd.VirtualPath = GetUrlWithApplicationPath(requestContext, vpd.VirtualPath);
                return vpd;
            } 
        }
    } 

    return null;
} 

public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values) {
    requestContext = GetRequestContext(requestContext);

    if (!String.IsNullOrEmpty(name)) {
        RouteBase namedRoute; 
        bool routeFound; 
        using (GetReadLock()) {
            routeFound = _namedMap.TryGetValue(name, out namedRoute); 
        }
        if (routeFound) {
            VirtualPathData vpd = namedRoute.GetVirtualPath(requestContext, values);
            if (vpd != null) { 
                vpd.VirtualPath = GetUrlWithApplicationPath(requestContext, vpd.VirtualPath);
                return vpd; 
            } 
            return null;
        } 
        else {
            throw new ArgumentException(
                String.Format(
                    CultureInfo.CurrentUICulture, 
                    RoutingResources.RouteCollection_NameNotFound,
                    name), 
                "name"); 
        }
    } 
    else {
        return GetVirtualPath(requestContext, values);
    }
} 

You can choose a named route or use all routes to build a url. It delegate build url to Route.GetVirtualPath method

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
    // Try to generate a URL that represents the values passed in based on current 
    // values from the RouteData and new values using the specified Route.

    BoundUrl result = _parsedRoute.Bind(requestContext.RouteData.Values, values, Defaults, Constraints);

    if (result == null) {
        return null; 
    } 

    // Verify that the route matches the validation rules 
    if (!ProcessConstraints(requestContext.HttpContext, result.Values, RouteDirection.UrlGeneration)) {
        return null;
    }

    VirtualPathData vpd = new VirtualPathData(this, result.Url);

    // Add the DataTokens from the Route to the VirtualPathData 
    if (DataTokens != null) {
        foreach (var prop in DataTokens) { 
            vpd.DataTokens[prop.Key] = prop.Value;
        }
    }
    return vpd; 
}

Route is importaint object, it is used in url generation. It has Constraints, DataTokens, Defaults properties.

Nov 10, 2009

HttpContextBase.Items

When you using controller, you can easily access HttpContext property, which is not an System.Web.HttpContext, but a HttpContextBase object. This object has a handy Items dictionary, which can allowed you share data over a the whole request.

TempData of Controller

TempData is property of Controller. The TempData is used to save the state from page redirected from to the page redirected. So let say, your request is handled by controller1.method1, for some reason, it should be redirected to controller2.method2. Before the redirection take place, you can save some data in the TempData, then http status code 302 and new url are returned to browser, and browser make a new request using new url. In the new action method, the TempData can be restored. This behavior is showed in the following code in the Controller

//Controller member
protected override void ExecuteCore() {
    TempData.Load(ControllerContext, TempDataProvider);

    try {
        string actionName = RouteData.GetRequiredString("action");
        if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
            HandleUnknownAction(actionName);
        }
    }
    finally {
        TempData.Save(ControllerContext, TempDataProvider);
    }
}

This piece of code does not show how data is actually load and save, the job is actually performed by TempDataProvider. And the default provider is SessionStateTempDataProvider. We can override this TempDataProvider by injecting this property using custom ControllerFactory.

//Controller member
public ITempDataProvider TempDataProvider {
    get {
        if (_tempDataProvider == null) {
            _tempDataProvider = new SessionStateTempDataProvider();
        }
        return _tempDataProvider;
    }
    set {
        _tempDataProvider = value;
    }
}

//TempDataDictionary members
public void Load(ControllerContext controllerContext, ITempDataProvider tempDataProvider) {
    IDictionary<string, object> providerDictionary = tempDataProvider.LoadTempData(controllerContext);
    _data = (providerDictionary != null) ? new Dictionary<string, object>(providerDictionary, StringComparer.OrdinalIgnoreCase) :
        new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
    _initialKeys = new HashSet<string>(_data.Keys);
    _modifiedKeys.Clear();
}

public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider) {
   //this is very important, if there is no modified, we don't need to save it
   //for next use 
   if (_modifiedKeys.Count > 0) {

        // Apply change tracking.
        foreach (string x in _initialKeys) {
            if (!_modifiedKeys.Contains(x)) {
                _data.Remove(x);
            }
        }

        // Store the dictionary
        tempDataProvider.SaveTempData(controllerContext, _data);
    }
}

public class SessionStateTempDataProvider : ITempDataProvider {
    internal const string TempDataSessionStateKey = "__ControllerTempData";

    public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) {
        HttpContextBase httpContext = controllerContext.HttpContext;
        
        if (httpContext.Session == null) {
            throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled);
        }

        Dictionary<string, object> tempDataDictionary = httpContext.Session[TempDataSessionStateKey] as Dictionary<string, object>;

        if (tempDataDictionary != null) {
            // If we got it from Session, remove it so that no other request gets it
            httpContext.Session.Remove(TempDataSessionStateKey);
            return tempDataDictionary;
        }
        else {
            return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        }
    }

    public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) {
        HttpContextBase httpContext = controllerContext.HttpContext;

        if (httpContext.Session == null) {
            throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled);
        }

        httpContext.Session[TempDataSessionStateKey] = values;
    }        
}

Extension point in asp.net mvc

  1. Extending route.

    Route matching is first step mvc framework, we can customize how route is matched by adding constraint, and default, and dataToken. We can implement IRouteConstraint. Here is ASP.NET MVC Tip #30 – Create Custom Route Constraints

  2. Extending IRouteHandler

    When we register route, by default the route handler is MvcRouteHandler. We can create a class implmenting IRouteHandler, of we can sub class MvcRouteHandler

  3. Exending MvcHandler

    MvcHandler is IHttpHandler, we can substitute with a MvcHandler sub class, or a complete new IHttpHandler , or override MvcHandler's ProcessRequest method. We can override ProcessRequest(HttpContextBase httpContext) method.

  4. Extending ControllerFactory and Controller. We can decide how a controller is created. A dummy example is as follow. But you can create a controller by using IoC container.

    ControllerBuilder.Current.SetControllerFactory(new ObjectControllerFactory());
    
    public class ObjectControllerFactory:DefaultControllerFactory
    {
    
        public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            if (controllerName.ToLower() == "object")
            {
                return new ObjectController(new object());
            }
            else
            {
                return base.CreateController(requestContext, controllerName);
            }
        }
    
    }
    
  5. Extending the ActionResult.

  6. Extending the Controller.

  7. Extending ModelBinder

  8. Extending ViewResult

  9. Extending ViewEngine, and IView

    We can subclass the default WebFormViewEngine, or implement and IViewEngine. The interesting method is FindView and FindPartialView.

  10. Extending ViewPage or ViewUserControl

  11. Extending HtmlHelper by adding HtmlHelper extension method.

How a view is found

There are some confusing names here. ViewResult is kind of ActionResult. It is return by an object of Controller class. ViewResult uses VewEngineCollection, a collection of IViewEngine, to find a ViewEngineResult. ViewEngineResult may or may not contain an IView. IView is implemented by ViewPage, ViewUserControl.

After a action result is returned from the action method, if it is ViewResult. The render method will find an ViewEngineResult through ViewEngineCollection object. The following code show how ViewEngineCollection finds ViewEngineResult, ViewEngineCollection enumerate all the ViewEngine to fine ViewEngineResult.

//ViewEngineCollection member
private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> cacheLocator, Func<IViewEngine, ViewEngineResult> locator) {
    ViewEngineResult result;

    foreach (IViewEngine engine in Items) {
        if (engine != null) {
            result = cacheLocator(engine);

            if (result.View != null) {
                return result; //With IView
            }
        }
    }

    List<string> searched = new List<string>();

    foreach (IViewEngine engine in Items) {
        if (engine != null) {
            result = locator(engine);

            if (result.View != null) {
                return result; //With IView
            }

            searched.AddRange(result.SearchedLocations);
        }
    }
    //a ViewEngineResult with no IView
    return new ViewEngineResult(searched);
}

If the ViewEngineResult return by by IViewEngine has an IView object, it will be returned immediately, it will be returned immediately to the ViewResult caller. If you want to use your special IView object, you need to add IViewEngine to locate your IView. If there is no ViewEngineResult with an IView is found, return new ViewEngineResult with no IView to caller ViewResult, let see how ViewResult handle this.

protected override ViewEngineResult FindView(ControllerContext context) {
    ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
    if (result.View != null) {
        return result;
    }

    // we need to generate an exception containing all the locations we searched
    StringBuilder locationsText = new StringBuilder();
    foreach (string location in result.SearchedLocations) {
        locationsText.AppendLine();
        locationsText.Append(location);
    }
    throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
        MvcResources.Common_ViewNotFound, ViewName, locationsText));
}

It throw an exception. If a ViewEngineResult with IView is returned, ViewResult will render the IView.

//ViewResultBase member
public override void ExecuteResult(ControllerContext context) {
    if (context == null) {
        throw new ArgumentNullException("context");
    }
    if (String.IsNullOrEmpty(ViewName)) {
        ViewName = context.RouteData.GetRequiredString("action");
    }

    ViewEngineResult result = null;

    if (View == null) {
        result = FindView(context);
        View = result.View;
    }

    ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
    View.Render(viewContext, context.HttpContext.Response.Output);

    if (result != null) {
        result.ViewEngine.ReleaseView(context, View);
    }
}

Here is example to how to implement an IView.

public class StaticView : IView
{

    public void Render(ViewContext viewContext, System.IO.TextWriter writer)
    {
        writer.Write("

Hello world

"); } } public class StaticViewEngine : IViewEngine { public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) { ViewEngineResult result = new ViewEngineResult(new StaticView(), this); return result; } public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) { ViewEngineResult result = new ViewEngineResult(new StaticView(), this); return result; } public void ReleaseView(ControllerContext controllerContext, IView view) { //do nothing } } protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ViewEngines.Engines.Insert(0, new StaticViewEngine()); }

Nov 9, 2009

Context data in asp.net mvc

Along the asp.net mvc stack, there are lots context data being passing around. The first context data is passed into MvcRouteHandler by the System.Web.Routing.UrlRoutingModule.

public class RequestContext
{
    public RequestContext(HttpContextBase httpContext, RouteData routeData);

    public HttpContextBase HttpContext { get; }
    public RouteData RouteData { get; }
}

Then the RequestContext is passed into MvcHandler via constructor. When MvcHandler.ProcessRequest is called, it wrap the httpContext into HttpContextBase, which is System.Web.Abstractions class. The reason to use HttpContext to replace the dependency on HttpContext object, which is sealed, with dependency on HttpContextBase which is an abstraction.

//MvcHandler member
public MvcHandler(RequestContext requestContext) {
    if (requestContext == null) {
        throw new ArgumentNullException("requestContext");
    }
    RequestContext = requestContext;
}

protected virtual void ProcessRequest(HttpContext httpContext) {
    HttpContextBase iHttpContext = new HttpContextWrapper(httpContext);
    ProcessRequest(iHttpContext);
}

protected internal virtual void ProcessRequest(HttpContextBase httpContext) {
 //skip
 IController controller = factory.CreateController(RequestContext, controllerName);
 //skip
  controller.Execute(RequestContext);
}

When creating Controller, the RequestContext is passed into the ControllerFactory.CreateController method, after the controller is created the RequestContext is passed into controller.Execute method. By default, the controller is a System.Web.Mvc.Controller. A ControllerContext is created based on the RequestContext, the controller itself. A UrlHelper is also created with RequestContext. So nowt the Controller has a ControllerContext, and UrlHelper.

//Controller member
protected override void Initialize(RequestContext requestContext) {
    base.Initialize(requestContext);
    Url = new UrlHelper(requestContext);
}
//ControllerBase member
protected virtual void Initialize(RequestContext requestContext) {
    ControllerContext = new ControllerContext(requestContext, this);
}

Then ControllerContext is used to create ControllerDescriptor, and ActionDescriptor, and FilterInfo(Filters), AuthorizationContext, parameters for actio method. ActionExecutedContext. It is also used in InvokeActionResultWithFilters method. Eventually, ActionResult use ControllerContext to ExecuteResult. ContextController is used to find ViewEngineResult

public abstract class ActionResult {

    public abstract void ExecuteResult(ControllerContext context);

}

A ViewContext is created by using ControllerContext, and IView use viewContext to render view. ViewContext.ViewData is passed into ViewPage or ViewUserControl

acton parameters

The asp.net mvc framework matches action parameters in the following order:

  1. Form values
  2. Route arguments
  3. Querystring parameters

The parameters that is passed in the action method is handled by ControllerActionInvoker.The ControllerActionInvoker tries to using reflection and other techniques to extract information for client's request, as the source code shows below.

//ControllerActionInvoker members
public bool InvokeAction(ControllerContext controllerContext, string actionName)
{
//..code skip
IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
//..
}
protected virtual IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
    Dictionary<string, object> parametersDict = new Dictionary(StringComparer.OrdinalIgnoreCase);
    ParameterDescriptor[] parameterDescriptors = actionDescriptor.GetParameters();

    foreach (ParameterDescriptor parameterDescriptor in parameterDescriptors)
    {
        parametersDict[parameterDescriptor.ParameterName] = GetParameterValue(controllerContext, parameterDescriptor);
    }
    return parametersDict;
}

protected virtual object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
{
    // collect all of the necessary binding properties
    Type parameterType = parameterDescriptor.ParameterType;
    //you can register your binder, but default is DefaultModelBinder
    IModelBinder binder = GetModelBinder(parameterDescriptor);
    IDictionary<string, ValueProviderResult> valueProvider = controllerContext.Controller.ValueProvider;
    string parameterName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName;
    Predicate<string> propertyFilter = GetPropertyFilter(parameterDescriptor);

    // finally, call into the binder
    ModelBindingContext bindingContext = new ModelBindingContext()
    {
        FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified
        ModelName = parameterName,
        ModelState = controllerContext.Controller.ViewData.ModelState,
        ModelType = parameterType,
        PropertyFilter = propertyFilter,
        ValueProvider = valueProvider
    };
    object result = binder.BindModel(controllerContext, bindingContext);
    return result;
}

You can also register your ModelBinder to help mvc to build your parameter in the application startup like the code below.

public class ConferenceModelBinder : DefaultModelBinder
{
 private readonly IConferenceRepository _repository;

 //We can require dependencies in binders just like normal classes
 public ConferenceModelBinder(IConferenceRepository repository)
 {
  _repository = repository;
 }

 public override object BindModel(ControllerContext controllerContext,
                                  ModelBindingContext bindingContext)
 {
  //first, we get the value from the request that matches the name
  ValueProviderResult providerResult =
   bindingContext.ValueProvider[bindingContext.ModelName];

  //next we use our repository to find the Conference by the key
  Conference conference = _repository.GetByKey(providerResult.AttemptedValue);
  return conference;
 }
}

ModelBinders.Binders.Add(typeof (Conference),
new ConferenceModelBinder(
new ConferenceRepositoryStub()));

Nov 8, 2009

The options to render a partial view

Normally you can do this,

<% Html.RenderPartial("LogOnUserControl"); %>

Or you can do this

ViewResult partialResult = View("next");
return View(partialResult);
<%ViewData.Model.ExecuteResult(ViewContext); %>

Nov 6, 2009

asp.net mvc request walkthrough

ASP.NET MVC request begin with a UrlRoutingModule. This component is not actually part of the System.Web.Mvc.dll, but of System.Web.Routing. You have to wire up in web.config like the following.

<httpModules>
 <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</httpModules>  

After that you can define your matching url pattern and the IRouteHandler which handle the matched url. You can add route like the following code.

Route route = new Route("{controller}/{action}/{id}", new MvcRouteHandler()) ;
route.Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", id = "" });
RouteTable.Routes.Add("Default", route);

Of course, you can add the use the extension method define in System.Web.Mvc.RouteCollectionExtensions. You can add a route with your own IRouteHandler. The purpose of IRouteHandler is very simple, to create a IHttpHandler.

public class MvcRouteHandler : IRouteHandler {
    protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) {
        return new MvcHandler(requestContext);
    }

    #region IRouteHandler Members
    IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) {
        return GetHttpHandler(requestContext);
    }
    #endregion
}

The RequestContext object is interesting object.It contains a HttpContextBase object and a RouteData object.This object will be used through out the mvc stack. You can also change the mvc behavior by subclassing the MvcHandler.

protected virtual void ProcessRequest(HttpContext httpContext) {
    HttpContextBase iHttpContext = new HttpContextWrapper(httpContext);
    ProcessRequest(iHttpContext);
}

protected internal virtual void ProcessRequest(HttpContextBase httpContext) {
    AddVersionHeader(httpContext);

    // Get the controller type
    string controllerName = RequestContext.RouteData.GetRequiredString("controller");

    // Instantiate the controller and call Execute
    IControllerFactory factory = ControllerBuilder.GetControllerFactory();
    IController controller = factory.CreateController(RequestContext, controllerName);
    if (controller == null) {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.ControllerBuilder_FactoryReturnedNull,
                factory.GetType(),
                controllerName));
    }
    try {
        controller.Execute(RequestContext);
    }
    finally {
        factory.ReleaseController(controller);
    }
}

The IControllerFactory by default is DefaultControllerFactory. This can be changed in the application_startup method. The _factoryThunk is an Func delegate. ControllerFactory is used to build a controller. Sometimes, you need to use IoC to inject dependency to into the container, if so you need to create your ControllerFactory.

ControllerBuilder.Current.SetControllerFactory(new 
StructureMapControllerFactory());

public class ControllerBuilder {
 //.. other members
public void SetControllerFactory(IControllerFactory controllerFactory) {
    if (controllerFactory == null) {
        throw new ArgumentNullException("controllerFactory");
    }

    _factoryThunk = () => controllerFactory;
}

public void SetControllerFactory(Type controllerFactoryType) {
    if (controllerFactoryType == null) {
        throw new ArgumentNullException("controllerFactoryType");
    }
    if (!typeof(IControllerFactory).IsAssignableFrom(controllerFactoryType)) {
        throw new ArgumentException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.ControllerBuilder_MissingIControllerFactory,
                controllerFactoryType),
            "controllerFactoryType");
    }

    _factoryThunk = delegate() {
        try {
            return (IControllerFactory)Activator.CreateInstance(controllerFactoryType);
        }
        catch (Exception ex) {
            throw new InvalidOperationException(
                String.Format(
                    CultureInfo.CurrentUICulture,
                    MvcResources.ControllerBuilder_ErrorCreatingControllerFactory,
                    controllerFactoryType),
                ex);
        }
    };
}

}

The most commonly used controller is System.Web.Mvc.Controller, which inherit ControllerBase class. It has a property ControllerContext, which holds a RequestContext object, and a reference the Controller itself. MvcHandler will call the controller's Execute(RequestContext) method, which calls ExecuteCore method. As a developer, you need to implement your action method, the ExecuteCore method will try to call you action method.

protected override void ExecuteCore() {
    TempData.Load(ControllerContext, TempDataProvider);

    try {
        string actionName = RouteData.GetRequiredString("action");
        if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
            HandleUnknownAction(actionName);
        }
    }
    finally {
        TempData.Save(ControllerContext, TempDataProvider);
    }
}

public IActionInvoker ActionInvoker {
    get {
        if (_actionInvoker == null) {
            _actionInvoker = new ControllerActionInvoker();
        }
        return _actionInvoker;
    }
    set {
        _actionInvoker = value;
    }
}

The ActionInvoker is also an extension point, you can set this property when the controller is created, you can do this in your ControllerFactory. By default, it is ControllerActionInvoker. Below is the code how ControllerActionInvoker select a action method, and invoking it. This is most interesting part.

// class ControllerActionInvoker
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) {
    if (controllerContext == null) {
        throw new ArgumentNullException("controllerContext");
    }
    if (String.IsNullOrEmpty(actionName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
    }
    //this will return ReflectedControllerDescriptor, which is used to expose
   //some meta of the controller, which implement System.Reflection.ICustomAttributeProvider
    ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
    //Use controllerDescriptor build a ActionDescripter which is a ReflectedActionDescriptor, which also implements ICustomAttributeProvider
    ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
    if (actionDescriptor != null) {
        //FilterInfo has four list,ActionFilters(IActionFilter), AuthorizationFilters(IAuthorizationFilter), ExceptionFilters(IExceptionFilter), ResultFilters(IResultFilter), these list contains FilterAttribute, but also the controller itself, because Controller implements  IActionFilter, IAuthorizationFilter, IExceptionFilter, IResultFilter
        FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);

        try {
            AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
            if (authContext.Result != null) {
                // the auth filter signaled that we should let it short-circuit the request
                InvokeActionResult(controllerContext, authContext.Result);
            }
            else {
                if (controllerContext.Controller.ValidateRequest) {
                    ValidateRequest(controllerContext.HttpContext.Request);
                }
                //parameters can build from controllerContext, and will be feed to actionMethod later.
                IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
                //your action method will be executed, and ActionResult will be 
                //returned here, which normally is ViewResult
                ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
                //the ActionExecutedContext.Result will be examined, if it is ActionResult, it will be invoked to render a view
                InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
            }
        }
        catch (ThreadAbortException) {
            // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
            // the filters don't see this as an error.
            throw;
        }
        catch (Exception ex) {
            // something blew up, so execute the exception filters
            ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
            if (!exceptionContext.ExceptionHandled) {
                throw;
            }
            InvokeActionResult(controllerContext, exceptionContext.Result);
        }

        return true;
    }
    // notify controller that no method matched
    return false;
}

There are lots implementation details here, I am going to skip most of them here. And focus on how a view is rendered. When ActionResult is returned from the method call InvokeActionMethodWithFilters, it will be passed in the method InvokeActionResultWithFilters, that is the beginning of rendering a view. The following code will be eventually run.

//ControllerActionInvoker member
protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
{
    actionResult.ExecuteResult(controllerContext);
}

ViewResult inherit from ViewResultBase, which inherite ActionResult. The interesting method is ExecuteResult(ControllerContext), it basically to find a view and then render the view.

//ViewResultBase member
public override void ExecuteResult(ControllerContext context) {
    if (context == null) {
        throw new ArgumentNullException("context");
    }
    if (String.IsNullOrEmpty(ViewName)) {
        ViewName = context.RouteData.GetRequiredString("action");
    }

    ViewEngineResult result = null;

    //View is property Of ViewResultBase, it is Type IView
    if (View == null) {
        result = FindView(context);
        View = result.View;
    }
    //push the ViewData, TempData into viewCotext, which is passed into View.Render()
    ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
    View.Render(viewContext, context.HttpContext.Response.Output);

    if (result != null) {
        result.ViewEngine.ReleaseView(context, View);
    }
}

public interface IView {
    void Render(ViewContext viewContext, TextWriter writer);
}

//ViewResult member
protected override ViewEngineResult FindView(ControllerContext context) {
    //ViewEngineCollection
    ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
    if (result.View != null) {
        return result;
    }

    // we need to generate an exception containing all the locations we searched
    StringBuilder locationsText = new StringBuilder();
    foreach (string location in result.SearchedLocations) {
        locationsText.AppendLine();
        locationsText.Append(location);
    }
    throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
        MvcResources.Common_ViewNotFound, ViewName, locationsText));
}

//ViewResultBase member, this is an extension point
//we can add our ViewEngine here.
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
    Justification = "This entire type is meant to be mutable.")]
public ViewEngineCollection ViewEngineCollection {
    get {
        return _viewEngineCollection ?? ViewEngines.Engines;
    }
    set {
        _viewEngineCollection = value;
    }
}

public static class ViewEngines {

    private readonly static ViewEngineCollection _engines = new ViewEngineCollection {
        new WebFormViewEngine() 
    };

    public static ViewEngineCollection Engines {
        get {
            return _engines;
        }
    }
}

We can see here, to find a view, we need a ViewEngineCollection. By default this ViewEngineCollection has one IViewEngin(WebFormViewEngine). The purpose a IViewEngine is to find partial view and view, like the following. But WebFormViewEngine inherit VirtualPathProviderViewEngine, which implmenet IViewEngine. To find View, first we need to locate the Physical path of the view file(aspx or ascx), to decide whether a file exist, there is to ask a buildManager to build an instance of it. Physical path searhing follow a couple of pattern, if the view file can not be located. Then an exeption will be throw. If found a the path will be cached for next probing.

public interface IViewEngine {
    ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
    void ReleaseView(ControllerContext controllerContext, IView view);
}

//VirtualPathProviderViewEngine member
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) {
    if (controllerContext == null) {
        throw new ArgumentNullException("controllerContext");
    }
    if (String.IsNullOrEmpty(viewName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
    }

    string[] viewLocationsSearched;
    string[] masterLocationsSearched;

    string controllerName = controllerContext.RouteData.GetRequiredString("controller");
    string viewPath = GetPath(controllerContext, ViewLocationFormats, "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched);
    string masterPath = GetPath(controllerContext, MasterLocationFormats, "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched);

    if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName))) {
        return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
    }

    return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}


//WebFormViewEngine constructor
public WebFormViewEngine() {
    MasterLocationFormats = new[] {
        "~/Views/{1}/{0}.master",
        "~/Views/Shared/{0}.master"
    };

    ViewLocationFormats = new[] {
        "~/Views/{1}/{0}.aspx",
        "~/Views/{1}/{0}.ascx",
        "~/Views/Shared/{0}.aspx",
        "~/Views/Shared/{0}.ascx"
    };

    PartialViewLocationFormats = ViewLocationFormats;
}

The WebFormViewEngine defines how to create view, it implement two abstract method of VirtualPathProviderViewEngine.

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) {
    return new WebFormView(partialPath, null);
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) {
    return new WebFormView(viewPath, masterPath);
}

The WebFormView use tranditional aspx ViewPage or ViewUserControl ascx to render view.

//WebFormView  members
public virtual void Render(ViewContext viewContext, TextWriter writer) {
    if (viewContext == null) {
        throw new ArgumentNullException("viewContext");
    }
    
    //viewInstance is an instance of ViewPage(aspx or ascx)
    object viewInstance = BuildManager.CreateInstanceFromVirtualPath(ViewPath, typeof(object));
    if (viewInstance == null) {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentUICulture,
                MvcResources.WebFormViewEngine_ViewCouldNotBeCreated,
                ViewPath));
    }


    ViewPage viewPage = viewInstance as ViewPage;
    if (viewPage != null) {
        RenderViewPage(viewContext, viewPage);
        return;
    }
    //we can see that viewInstance does not necessary to be a ViewPage, it can be ViewUserControl as well
    ViewUserControl viewUserControl = viewInstance as ViewUserControl;
    if (viewUserControl != null) {
        RenderViewUserControl(viewContext, viewUserControl);
        return;
    }

    throw new InvalidOperationException(
        String.Format(
            CultureInfo.CurrentUICulture,
            MvcResources.WebFormViewEngine_WrongViewBase,
            ViewPath));
}

private void RenderViewPage(ViewContext context, ViewPage page) {
    if (!String.IsNullOrEmpty(MasterPath)) {
        page.MasterLocation = MasterPath;
    }
    //passed in ViewData to aspx or ascx
    page.ViewData = context.ViewData;
    page.RenderView(context);
}

private void RenderViewUserControl(ViewContext context, ViewUserControl control) {
    if (!String.IsNullOrEmpty(MasterPath)) {
        throw new InvalidOperationException(MvcResources.WebFormViewEngine_UserControlCannotHaveMaster);
    }

    //passed in ViewData to aspx or ascx
    control.ViewData = context.ViewData;
    control.RenderView(context);
}

To render a ViewPage, firstly, it initialized three very handy object will be used extensively in the aspx or ascx, they are Ajax(AjaxHelper), Html(HtmlHelper),Url(UrlHelper). Then it delegate the control to ProcessRequest method of Page class.

//public class ViewPage : Page, IViewDataContainer
public virtual void RenderView(ViewContext viewContext) {
    ViewContext = viewContext;
    InitHelpers();
    // Tracing requires Page IDs to be unique.
    ID = Guid.NewGuid().ToString();
    ProcessRequest(HttpContext.Current);
}


public virtual void InitHelpers() {
    Ajax = new AjaxHelper(ViewContext, this);
    Html = new HtmlHelper(ViewContext, this);
    Url = new UrlHelper(ViewContext.RequestContext);
}

HtmlHelper is responsible to render partial view.

internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, object model, ViewEngineCollection viewEngineCollection) {
    if (String.IsNullOrEmpty(partialViewName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
    }
    
    //a new copy a ViewData is created 
    ViewDataDictionary newViewData = null;

    if (model == null) {
        if (viewData == null) {
            newViewData = new ViewDataDictionary(ViewData);
        }
        else {
            newViewData = new ViewDataDictionary(viewData);
        }
    }
    else {
        if (viewData == null) {
            newViewData = new ViewDataDictionary(model);
        }
        else {
            newViewData = new ViewDataDictionary(viewData) { Model = model };
        }
    }
    //a newViewContext is created, not shared with passed in data
    ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, newViewData, ViewContext.TempData);
    //use the passed in viewEngineCollectio to newViewContext to find a partial view
    IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection);
    view.Render(newViewContext, ViewContext.HttpContext.Response.Output);
}

//ViewDataDictionary member
[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors",
    Justification = "See note on SetModel() method.")]
public ViewDataDictionary(ViewDataDictionary dictionary) {
    if (dictionary == null) {
        throw new ArgumentNullException("dictionary");
    }

    foreach (var entry in dictionary) {
        _innerDictionary.Add(entry.Key, entry.Value);
    }
    foreach (var entry in dictionary.ModelState) {
        ModelState.Add(entry.Key, entry.Value);
    }
    Model = dictionary.Model;
}

Nov 1, 2009

Semantics works?

A developer is a creativity entity. There are so many software technologies emerging everyday, many of us are struggling to keep pace with the them. I am just an average developer, and I still tirelessly studying these technologies, because programming is my passion and love. Gradually, I understand semantics is the driven force of the evolution of software tools, framework, platform, methodologies and standards. As a .net developer, I use the object-oriented programming, domain driven development, agile programing, mocking framework, asp.net mvc, css, xhtml, wpf, RESTful web service.

I truely believe that semantics works. Today I am moving my studying notes to SemanticsWorks.