MVC3 Controller and view reusability
I'm currently working on set of MVC3 web applications that have a lot in common but are deployed as separate packages. One exception being an admin application that is deployed for each of installations. They also share a database and a set of common entities.
The recent additions included authentication (customized forms authentication), automated menu building (using controller action attributes) and few other screens. The issue that I have with these is I had to create duplicated controllers and views.
Best example being the authentication bit. Each app now has a Account controller and Authenticate view that are copies of each other. You can see how this can quickly become a maintenance nightmare as there are more applications added. Another example is a shared view in each application that renders a menu (usually called via Html.Action). This also needs a controller which looks exactly the same in each of applications.
I did manage to abstract the implementation details of authentication and menu building to a shared project called XXX.Core.Mvc which also has common htmlHelper extensions and anything specific to Mvc web. So now all the duplicated views and controllers in applications act as proxies.
I'm looking开发者_JS百科 for a good pattern on how to get rid of these so the common views and controllers would reside in a shared project where they can be referenced / invoked. Has anyone done something like this? Can you recommend any good articles or examples?
For example how do you point a controller action that returns a View ActionResult to a view in a different project or shared path? How to route for example /Account/Authenticate to a controller in a different project? What are the deployment implications of an approach?
I tried two things. In each case the common controller and views were located in a separate assembly. For proof of concept I implemented common application error handling. So in my end point Mvc3 application, global.asax implements:
protected void Application_Error()
{
ApplicationErrorHandler.Get().Handle(Server, new HttpResponseWrapper(Response), new HttpContextWrapper(Context));
}
Where Application error handler is implemented in a separate assembly:
public class ApplicationErrorHandler
{
public static ApplicationErrorHandler Get()
{
return new ApplicationErrorHandler();
}
public void Handle(HttpServerUtility server, HttpResponseBase response, HttpContextBase context)
{
var exception = server.GetLastError();
var httpException = exception as HttpException;
response.Clear();
server.ClearError();
var routeData = new RouteData();
routeData.Values["controller"] = "Errors";
routeData.Values["action"] = "Http500";
routeData.Values["exception"] = exception;
response.StatusCode = 500;
if (httpException != null)
{
response.StatusCode = httpException.GetHttpCode();
switch (response.StatusCode)
{
case 403:
routeData.Values["action"] = "Http403";
break;
case 404:
routeData.Values["action"] = "Http404";
break;
}
}
IController errorsController = new ErrorsController();
var rc = new RequestContext(context, routeData);
errorsController.Execute(rc);
}
}
In this same assembly in Controllers folder resides the ErrorsController and Views\Errors holds the 3 views needed; Http403.cshtml, Http404.cshtml and Http405.cshtml. SO the structure is the same as in your normal Mvc3 project. However in order to get @model support and intelisense to work you will also need web.config - a copy of the file from Views in your regular project and placed under views in this separate assembly.
When say an application error occurs in our application (http status 500 - internal server error) the ErrorsController is executed and the view used in this case will be Http500.cshtml. The path looked into will be:
- ~\Views\Errors\Http500.cshtml
- ~\View\Shared\Http500.cshtml
In order to supply the file from assembly I tried two things:
Approach one: Implement a VirtualPathProvider. I will not go into details but the goal is to embed the cshtml views as resources and serve them up using VirtualPathProvider and VirtualFile. The issue with this approach is I found no feasible way to also server the web.config from Views folder in order to get Razor to evaluate the view. I could get it to retrieve the file and stream it but not execute it. To explain what the issue was, consider the Http500.cshtml:
@model Exception
<h3>@MvcHtmlString.Create(Model.Message.Replace(Environment.NewLine, "<br />").Replace("\t", " "))</h3>
<p>@MvcHtmlString.Create(Model.StackTrace.Replace(Environment.NewLine, "<br />").Replace("\t", " "))</p>
<input type="hidden" name="serverError" value="true"/>
What was returned was the contents of the file rather than evaluated Exception. The reason is that the web.config from View folder needs to be served as well so that Razor is used. By default the view engine is web forms (.aspx).
Approach two: create virtual directory in IIS. This approach works out much better because the views do not have to be compiled as resources, there is no need for virtualization. All that is needed was to create virtual directory Errors under Views folder in my end application and point it to where the Views\Errors from common assembly is deployed.
Sure, it doesn't behave like a proper plugin because common files need to be deloyed separate from .dll but I'm not too bothered about that.
However if someone knows of a way on how to get virtual files to work for Razor I'm all ears. Well - all eyes really.
精彩评论