-
Notifications
You must be signed in to change notification settings - Fork 3
Rendering view models
After all services return view models for given page it's time to display their content. Because most probably each view model wants to invoke specific view, or action we have to able to route to this specific content, as these actions will be invoked from main application context. Furthermore in these views it's likely that we will want to execute other service specific actions, or template views.
In order to tell view model to render itself, we pass it current page, so view model can invoke some action, and optionally id of container html element, so that rendered widget could for example append its content after making ajax call.
Part of IViewModel class responsible for rendering view model:
public interface IViewModel
{
MvcHtmlString Execute(WebViewPage viewPage);
MvcHtmlString Execute(WebViewPage viewPage, string containerId);
}
Most probably view model will want to execute some service specific view, or action, so when this call will be executed, main application routing will search for route that will show what should be invoked. As a result, in order to execute service view, or action, each service has to register itself in main application's routing. Because each service is developed independently, there can be multiple routes that match the same view or action request, so each service must register itself in unique way.
To solve this, we register each service in main application routing with unique key - for example service name, as it is unique in whole system. In this way in global app's routing there will be multiple routes that can handle the same route, but each of them will fire only when specific key will match their own key.
Example of registering service routing with service key:
public class TestAreaAreaRegistration : ServiceAreaRegistration
{
public override string AreaName
{
get
{
return "TestArea";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
CreateRoute( //creates custom route that will fire only when service key is passed
context,
CustomersConsts.ServiceName + "_TestArea_default",
CustomersConsts.ServiceName + "/TestArea/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
protected override string RouteServiceValue //service specific key
{
get { return CustomersConsts.RouteServiceValue; }
}
}
In this way we can route to specific service actions or views, just by passing specifc key, so when view model's action will include this key, executing view model will render desired view, or execute desired action.
In order to render desired view we should specify all route values in view model's action - ie view name, controller name, eventually area name and service key - but this would be unnatural when we just want to render view from current controller and certainly we don't want to pass service key each time as this would be at least error prone. That's way ViewModel class accepts expression tree, not just simple Func. When service action returns its view models, ActionResult is executed by mvc infrastructure, so this is signal for ViewModel that can capture route values for current route. It saves current controller name, area name, model state, view data and of course service key that was included in internal route. Later on, when main page orders view model to execute itself, expression tree is traverse and every call to Html.Action extension method is updated to include default route value if not specified in service action and also it includes captured model state, view data and service key, so we can only specific action name that we want to invoke and rest of routing values will be filled from current request as in standard routing behavior.
There is also issue of fake invoking action - because invoking Html.RenderPartial copies parent view data into invoking view and we want to keep develop each service separately, most probably Html.Action will be specified to render view model. As a result we should create fake action method that will only accept model to render and it will invoke view. This is again unnatural, because we want to invoke view directly when with specific model. For doing this, each ServiceController contains action with unusual name called _View, so its unlikely that someone would want to call this action directly, that accepts parameters such as view name to render, model for render, model state, view data and id of container html element. Because we don't even know about this action, when view model is executed and expression tree is traversed to update every Html.Action call, it also swaps action name from the one specified to _View, and the real one is passed as parameter. As a result, when specifing how to render view model, we can specify invoking Html.Action extension method, passing desired view name and optionally any route parameters including model for view, and it will be converted for rendering this specific view, despite Html.Action invoking. If we really want to invoke action - for example for caching purposes - we can set ViewModel's RenderAction property to true. In this way Html.Action will actually call given action and all captured parameters such as model state, view data and container id will be available through routing values inside that action.
Example of converting Html.Action
[InternalAction]
public ActionResult SomePage()
{
var someModel = //creating model for view
var viewModel = ("widgetId", page => page.Html.Action("SomeView", new { model = someModel });
//viewModel action will be converted into page => page.Html.Action("_View", new { controller = "currentController", area = "currentArea", model = someModel, serviceKey = "specificServiceKey", modelState = this.ModelState, viewData = this.ViewData, containerId = "passedContainerId" })
return ViewModel(viewModel);
}
[InternalAction]
public ActionResult SomePage()
{
var someModel = //creating model for view
var viewModel = ("widgetId", page => page.Html.Action("SomeView", new { controller = "someController", model = someModel });
//viewModel action will be converted into page => page.Html.Action("_View", new { controller = "someController", area = "currentArea", model = someModel, serviceKey = "specificServiceKey", modelState = this.ModelState, viewData = this.ViewData, containerId = "passedContainerId" })
return ViewModel(viewModel);
}
One important thing to note is when we want to pass model to view, we have to specify it as model parameter. If we pass it with different parameter name, then it won't be included in view.
There is also a matter of invoking other actions, or views from rendered view - they also must be from given service. Solution to this is automatically provided by asp.net mvc, because every child view or action invoking will include parent route values, so when service specifc view is invoked, farther calls will include service key by default, so we can execute views naturally as in web application that is hosted on different server.
In order to render service specific view, we have to be able to find its physical file. Again because services are developed independently, there can be many views with the same names. To solve this, we create view engine for every service, that will know how to find view for this service. Because when asp.net mvc wants to render view it asks every view engine to provide it, we have to be able to provide that will be fired view engine only for service that we ask for. For this we can make use of service key, because when view engine is asked for view, it gets view name and route for that view. So because when we route to service view, we include service key, then each view engine can check if service key specified in route matches its own service key. If not, this means that this request is not for this view engine and it doesn't search for view. If service key matches, then it looks for given view name in its locations.
Example of general view engine that every service view engine implements specifying its service key:
public abstract class ViewEngine : RazorViewEngine
{
public abstract string RouteServiceValue { get; } //service key
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
var searchedEngine = RouteServiceValue + "_ViewEngine";
var notFound = new ViewEngineResult(new[] { searchedEngine });
if (controllerContext.RouteData.DataTokens.ContainsKey(Consts.RouteServiceKey))
return (string)controllerContext.RouteData.DataTokens[Consts.RouteServiceKey] == RouteServiceValue ? base.FindPartialView(controllerContext, partialViewName, useCache) : notFound;
return notFound;
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var searchedEngine = RouteServiceValue + "_ViewEngine";
var notFound = new ViewEngineResult(new[] { searchedEngine });
if (controllerContext.RouteData.DataTokens.ContainsKey(Consts.RouteServiceKey))
return (string)controllerContext.RouteData.DataTokens[Consts.RouteServiceKey] == RouteServiceValue ? base.FindView(controllerContext, viewName, masterName, useCache) : notFound;
return notFound;
}
}
In many cases we want to display widgets using templates, because for example we want that every row of a form looks identically. However we don't want to specify each template in each service, so that it can be found by service view engine - we want to create it once in main web application, so that each service can use it.
To solve this we register all service view engines first and as last we register main web app view engine. This is way asp.net mvc looks for a view - it asks every registered view engine to provide it with view and it stops after getting it. When main app's view engine is the last one and we ask for service specific view, asp.net mvc will as every service view enegine if it can provide view - only one of them will fire, as only one of them has matching service key. If it can't find specific for given route, then main app's view engine will be asked for it.
As a result we can define template views in main application, so when service view engine will be asked for a view it won't find it and main app's view engine will. So we can use templates created in main application. Furthermore if any service wants to create its own template it can define it itself with the same name as main application's, so when view in this service will ask for template with this view, service specific view engine - because it is asked first - will find that tempalte view and render it. So we can override template views from main application in each service separately, because if other service won't override it, it will render template from main application.
-
Welcome
1.1 Project overview
1.2 Features -
Introduction
2.1 UI composition example
2.2 Service separation
2.3 Service communication
2.4 UI composition goals
2.5 Clues how to start
2.6 Potencial problems -
Identifying widgets
3.1 Naming widgets
3.2 Widgets format
3.3 Amount of widgets
3.4 Widgets and service boundaries
3.5 Widgets and caching
3.6 Grid for widgets -
Delivering view models
4.1 Publishing request
4.2 Internal routing
4.3 Data in view models
4.4 Gathering view models -
Rendering view models
5.1 External routing
5.2 Including route values
5.3 Finiding physical view files
5.4 Template views - Getting data from request
- Sharing resources
- Service api
-
Transactions
9.1 One transaction
9.2 Multiple transactions
9.3 Transaction performance - Dependency injection
- Public api changes
- [Tables] (https://github.com/padzikm/CompositeUI/wiki/Tables)
12.1 Table order
12.2 Server-side rendering
12.3 Client-side rendering
12.4 Nested tables -
Cross-service validation
13.1 Server-side validation
13.2 Client-side validation -
CRUD
14.1 Service private data
14.2 Create
14.3 Update
14.4 Delete
14.5 Preview -
Caching
15.1 View models
15.2 Widgets
15.3 Pages - Optimizing network calls
- Scalling
- Client-side communication
- Deployment
- Starting new project