0

Recently, I found myself copying views around in ASP.NET MVC application and changing nothing or just a few things, generally name of controller on BeginForm("actionName", "controllerName") (and thankfully, this can be solved inside view). Shortly, my main purpose is to eliminate duplicate views.

Reason

The reason for this is that I have different controllers with similar functionality. They seem same to user but calculations are very different in controllers. More specifically I have four controllers called InitialReport, CorrectionReport, InitialReportReview, CorrectionReportReview. The views of InitialReport, CorrectionReport and InitialReportReview, CorrectionReportReview are almost identical. So what I want to do is to eliminate duplicate views and create shared views for couples.

Problem

My problem is that, I do not want to put all views to the Shared view folder, because they are too many to put to this folder. Additionally, names of actions in controllers overlaps. Say I have ReportPayments action method in all four controllers, so name of views also overlaps. But the view must be different for InitialReport and InitialReportReview for example. So I have added two folders to Shared view folder called Report (for InitialReport, CorrectionReport) and ReportReview (for InitialReportReview, CorrectionReportReview). I plan to add all the shared views to Report and ReportReview sub-folders respectively. Now the problem is that, view look up locations must dynamically change to suit "controller type". For example, if I'm in InitialReport controller and I am navigation to ReportPayments action, then the view must be loaded from ~/Views/Shared/Report/ReportPayments.cshtml, but if I'm in InitialReportReview controller, then the view should be loaded from ~/Views/Shared/ReportReview/ReportPayments.cshtml.

Solution

As a solution I have created custom view engine, I override FindView method and set the ViewLocationFormats according to controller name.

public class ExtendedRazorViewEngine
    : RazorViewEngine
{
    string[] DefaultViewLocations { get; set; }
    string[] ReportViewLocations { get; set; }
    string[] ReportReviewViewLocations { get; set; }

    public ExtendedRazorViewEngine()
    {
        // Get the copy of default view locations
        DefaultViewLocations = new string[ViewLocationFormats.Length];
        ViewLocationFormats.CopyTo(DefaultViewLocations, 0);

        // Initialize ReportViewLocations
        List<string> customReportViewLocations = new List<string>
        {
            "~/Views/Shared/Report/{0}.cshtml"
        };
        ReportViewLocations = DefaultViewLocations
            .Union(customReportViewLocations)
            .ToArray();

        // Initialize ReportReviewViewLocations
        List<string> customReportReviewViewLocations = new List<string>
        {
            "~/Views/Shared/ReportReview/{0}.cshtml"
        };
        ReportReviewViewLocations = DefaultViewLocations
            .Union(customReportReviewViewLocations)
            .ToArray();
    }

    public override ViewEngineResult FindView(
        ControllerContext controllerContext, 
        string viewName, 
        string masterName, 
        bool useCache)
    {
        // Get controller name
        string controllerName = controllerContext.RouteData.GetRequiredString("controller");

        // Set view search locations
        if (controllerName.EndsWith("ReportReview"))
            ViewLocationFormats = ReportReviewViewLocations;
        else if (controllerName.EndsWith("Report"))
            ViewLocationFormats = ReportViewLocations;
        else
            ViewLocationFormats = DefaultViewLocations;

        return base.FindView(controllerContext, viewName, masterName, useCache);
    }
}

I have registered ExtendedRazorViewEngine as default view engine and it seems to work fine. But I have few questions. There is a question and answer which are similar (not identical) to mine, and quite helpful indeed, but it does not answer to my questions.

Questions

  1. Is this correct way? Does this method have any drawback such as views are not cached or any other performance issues? Can I make this method any better?
  2. Is there any better way of achieving my goal?
  3. When I tested, I realized that FindView method is hit twice. Why? I copied view engine and tested it in a almost empty MVC application, same thing happened there also, so I do not think it is related to my application.
Cœur
  • 37,241
  • 25
  • 195
  • 267
Adil Mammadov
  • 8,476
  • 4
  • 31
  • 59
  • This is overkill. Jesse Sierks is right - don't make it harder than it has to be. Use built-in options to provide view names, and extract differences into dynamically loaded partial views. You can store those names in - for example - ViewBag collection. – komsky Sep 30 '16 at 13:30
  • @komsky, thanks for your opinion, but it is not what I want. It is not an overkill, I want to write a good functional piece of code and get rid of repetition, specifying full view path in every action, is something which I do not want to see in my controller code. – Adil Mammadov Sep 30 '16 at 13:38

1 Answers1

1

I think you are making this harder than it needs to be. Putting the View in the Shared folder seems to be much simpler. But if you don't want to do that, you can still call a View even if it's not in the Shared folder.

From a view:

@Html.Partial("~/Views/ReportReview/InitialReport.cshtml")

Or from a Controller:

return this.View("~/Views/ReportReview/InitialReport.cshtml");

So if I were you, I would create these Views once, and just call that one from the other Views or Controllers. This way you don't have to duplicate code, and you don't have to create a custom view engine (which I imagine will be a bear to maintain over the years).

Jesse Sierks
  • 2,258
  • 3
  • 19
  • 30
  • Thanks for answer. As I explained in question, even if I want, I cannot put all views to shared folder, because view names overlaps, I need two different views called `ReportPayments` for example. And I don't want to specify exact view names in my controllers. If you imagine that I have dozens of actions then you can say that this is much *harder* and *uglier* solution. – Adil Mammadov Sep 30 '16 at 13:31
  • View names overlapping was the reason I thought the Shared folder would be easier/better for you. Just pass in a Model to the View that contains the differences (actions, controllers, etc.), or use the ViewBag instead of a Model. As long as the HTML is the same or essentially the same. Basically, this is exactly what the Shared views are for - preventing duplicated View code. Or I don't understand the question. – Jesse Sierks Sep 30 '16 at 13:40
  • No, I am afraid you didn't get the question right. Now I have four views called `ReportPayments`. Two for *InitialReport, ConcreteReport* (Report views) and two for *InitialReportReview, ConcreteReportReview* (Report review views). *Report views* are same with each other and *Report review views* are same with each other. Now, I need two views, one for *InitialReport, ConcreteReport* and one for *InitialReportReview, ConcreteReportReview*. – Adil Mammadov Sep 30 '16 at 13:44
  • So you have four Views called ReportPayments, each under a different Controller, and these Views are written the same? Same HTML and code? (Barring minor differences like class names or Form/Link Actions.) – Jesse Sierks Sep 30 '16 at 13:53
  • Do I miss telling something? Not all four are the same. Let me try to explain like: `InitalReport == ConcreteReport && InitialReportReview == ConcreteReportReview && InitalReport != InitialReportReview && ConcreteReport != ConcreteReportReview ` will result to `true` – Adil Mammadov Sep 30 '16 at 14:03