共用方式為


Appendix A: Changes to the server-side code

patterns & practices Developer Center

On this page: Download:
Reducing duplication in the controller actions | Using RenderAction - A few caveats, RenderPartial, Rendering forms, Hidden duplication, Following the pattern | Using content negotiation | Caveats Download code samples

Reducing duplication in the controller actions

In the early stages of the project, while the team was reviewing the source of the original Mileage Stats app from Project Silk, we decided that there was an opportunity to reduce the amount of code in the app without affecting the functionality. We were still operating under the self-imposed constraint of changing the original app as little as possible; however, making modifications to the controllers resulted in simplifying many of the problems we were facing for the mobile version of Mileage Stats.

In the original version of Mileage Stats there was a separate set of actions for delivering the JSON results needed for the single page application (SPA) experience. These actions were similar to their counterparts that were responsible for rendering markup. However, they differed in two significant ways. The first difference was variations in the models themselves. The second and closely related difference was that the actions returning markup needed to compose data from multiple sources in order to render the complete view.

For example, the original VehicleController contained the following two actions, both responsible for returning a list of vehicles:

…
public JsonResult JsonList()
{
    var list = Using<GetVehicleListForUser>()
        .Execute(CurrentUserId)
        .Select(x => ToJsonVehicleViewModel(x))
        .ToList();

    return Json(list);
}

public ActionResult List()
{
    AddCountryListToViewBag();

    var vehicles = Using<GetVehicleListForUser>()
        .Execute(CurrentUserId);

    var imminentReminders = Using<GetImminentRemindersForUser>()
        .Execute(CurrentUserId, DateTime.UtcNow);

    var statistics = Using<GetFleetSummaryStatistics>()
        .Execute(CurrentUserId);

    var model = new DashboardViewModel
                {
                    User = CurrentUser,
                    VehicleListViewModel = new VehicleListViewModel(vehicles),
                    ImminentReminders = imminentReminders,
                    FleetSummaryStatistics = statistics
                };
    return View(model);
}
…

Notice first that both actions make use of the same GetVehicleListForUser command. This command returns the primary data that both actions are concerned with. The JSON version projects the data to a new model using ToJsonVehicleViewModel. Whereas the markup version of the action collects additional data and then composes everything into an instance of the DashboardViewModel class. In fact, the class DashboardViewModel only exists to aggregate this data and to support IntelliSense in the corresponding view.

For the mobile version of Mileage Stats, we anticipated needing the same set of JSON endpoints. However, we knew after our initial design work that actions for the markup would be different. Specifically, the mobile version of the actions did not need the same data composed into the view model. For example, the original version of the Details action for the ReminderController included a list of vehicles:

…
public ActionResult Details(int id)
{
    var reminder = Using<GetReminder>()
        .Execute(id);

    var vehicles = Using<GetVehicleListForUser>()
        .Execute(CurrentUserId);

    var vehicle = vehicles.First(v => v.VehicleId == reminder.VehicleId);

    var reminders = Using<GetUnfulfilledRemindersForVehicle>()
        .Execute(CurrentUserId, reminder.VehicleId, vehicle.Odometer ?? 0)
        .Select(r => new ReminderSummaryModel(r, r.IsOverdue ?? false));

    var viewModel = new ReminderDetailsViewModel
                        {
                            VehicleList = new VehicleListViewModel(vehicles, vehicle.VehicleId) { IsCollapsed = true },
                            Reminder = ToFormModel(reminder),
                            Reminders = new SelectedItemList<ReminderSummaryModel>(reminders, x => x.First(item => item.ReminderId == id)),
                        };

    return View(viewModel);
}
…

In the final version of the action however, we only needed an instance of ReminderSummaryModel.

Rather than create a third set of actions with viewmodels specific to the mobile version of the app, we choose to consolidate all of the actions together and solve the problem of composition another way.

Using RenderAction

We decided to keep actions focused on their primary concern. Likewise, the name of the action should reflect this concern. For example, the List action of the ReminderController should return a list of reminders and nothing else. However, the view the desktop version would still need additional data. We could allow the view to compose the data itself using the RenderAction helper.

RenderAction allows a view to invoke additional actions and to include the results of those actions back in the originating view.

JJ149682.4A7EAAE31E357943B27BD35F7F76C02A(en-us,PandP.10).png

Using RenderAction can be a bit confusing; it helps to understand the workflow. An incoming request is handled normally, the action produces a model, and that model is passed to the view engine to render a view. When the view invokes RenderAction, Razor locates and executes the corresponding action (often called a child action). The child action produces a model, which is then used to render a secondary view. This secondary view is then inserted back into the original view at the point where RenderAction was invoked.

Using RenderAction, we were able to untangle the responsibilities in our actions and reuse the same set of actions between the original legacy version of Mileage Stats and the new mobily-friendly version.

A few caveats

There are few drawbacks to this approach. For example, you are invoking another controller action and there is always some performance cost when executing additional code. Though overall, the benefits greatly outweighed the problems in the context of Mileage Stats. Some additional considerations are as follows:

RenderPartial

It’s easy to confuse RenderAction with RenderPartial. RenderAction is for invoking a completely independent action. RenderPartial is simply for rendering a view based on a model passed to it. In most cases, the model passed to it is derived from the main view model.

Rendering forms

Avoid using RenderAction to render forms. It likely won’t work the way you’d expect because RenderAction essentially passes through the model view controller (MVC) lifecycle a second time. This means that any form rendering will need to occur in your primary view.

Hidden duplication

Since your controller actions are completely independent, it’s easy to unnecessarily duplicate expensive operations. For example, perhaps you have a view composing two actions that both need to look up details about the selected vehicle. If the data was aggregated at the controller level, it could consolidate the lookup. However, when the aggregation happens in the view, you might retrieve the same data twice.

Following the pattern

Using RenderAction breaks the model view controller pattern. It is generally assumed in MVC that the view does nothing more than render a model. It is the responsibility of a controller to invoke a view, but the view knows nothing about the controller. Using RenderAction breaks this rule.

Using content negotiation

RenderAction allowed us to consolidate the actions related to markup, but it did not address the fact that we had a separate set of similar actions for retrieving the JSON data. In fact, after redesigning the actions and views using RenderAction the resulting actions were even more similar to the JSON actions.

We decided to employ a technique known as content negotiation. Content negotiation allows a browser to specify the representation it would like for the resulting data. According to the HTTP specification, a client can identify a format that it prefers using the Accept header when making a request.

We created a custom ActionResult that would examine this header and render the result accordingly. Using this, the Details action on the ReminderController is able to handle requests for both JSON and markup.

…
public ActionResult Details(int vehicleId, int id)
{
    var reminder = Using<GetReminder>().Execute(id);
    var viewModel = new ReminderSummaryModel(reminder);

    return new ContentTypeAwareResult(viewModel);
}
…

Internally, the ContentTypeAwareResult examined the Accepts header of the incoming request for a value of "application/json". If this was found, then the viewModel was rendered using the standard JsonResult.

The following snippet demonstrates the flow of logic that the custom result uses to determine how to render its data. Note that WhenJson and WhenHtml are delegates used to perform the actual rendering. These delegates return a JsonResult and ViewResult, respectively.

…
// comments have been removed for brevity
private ActionResult GetActionResultFor(ControllerContext context)
{
    _supportedTypes = new Dictionary<string, Func<object, ViewDataDictionary, TempDataDictionary, ActionResult>>
                          {
                              {"application/json", WhenJson},
                              {"text/html", WhenHtml},
                              {"*/*", WhenHtml}, 
                          };

    var types = (from type in context.HttpContext.Request.AcceptTypes
                select type.Split(';')[0])
                .ToList();

    if (types.Count == 0)
    {
        var format = context.HttpContext.Request.QueryString["format"];
        var contentType = GetContentTypeForFormat(format);

        if (!string.IsNullOrEmpty(contentType))
        {
            types.Add(contentType);
        }
    }

    var providers = from type in types
                    where _supportedTypes.ContainsKey(type)
                    select _supportedTypes[type];

    if (providers.Any())
    {
        var getResult = providers.First();
        return getResult(_model, context.Controller.ViewData, context.Controller.TempData);
    }
    else
    {
        var msg = string.Format("An unsupported media type was requested. The supported content types are : {0}", String.Join(",", types));
        return new HttpStatusCodeResult(406, msg);
    }
}
…

It’s important to note that ContentTypeAwareResult was written to handle the specifics of Mileage Stats and is not meant to reflect a general purpose solution for content negotiation.

Caveats

The first caveat is that implementing content negotiation correctly is a large task. The problem itself seems trivial at first, but there are many edge cases. For example, our solution does not properly handle the weights associated with the accepted formats. If you want to use content negotiation, and especially if your API is consumed by clients outside of your control, then you should consider using a framework such as ASP.NET Web API.

The most significant problem we encountered, however, was the fact that certain mobile browsers would not allow us to set the Accept header on a request. Otherwise, these browsers met all of our requirements for delivering the SPA experience. Ultimately, we could not rely on the Accept header to accurately reflect the format that the client wanted. We resorted to appending the request format to the query string. You can see this fact in the snippet above, where we look for the presence of "format" in the query string.

Next Topic | Previous Topic | Home | Community

Last built: June 5, 2012