Question

Currently working on a legacy app. One section deals with generating "reports", each which share some common subsections, that are based on strongly-typed razor partial views. The largest report has something like 20 "partial views" for various subsections, most that occur in some other reports as well. Some of the partials require the outer, somewhat monolithic model in its entirety, and some require nested properties. However, many of those subsections also have some duplicate properties (but it's not universal), e.g.

class BigReportModel : GeneralReportStuff {
    SummaryData SummaryData {get; set;}
    OtherData OtherData {get; set;}
    string DesignName {get; set;}
    string UserName {get; set;}
    // 50 other properties
}

class SummaryData {
    string DesignName {get; set;}
    string UserName {get; set;}
    // 15 other unique properties
}

class OtherData {
    string DesignName {get; set;}
    // 10 other unique properties
}

// View
@model BigReportModel

<title>@Model.Title</title>
...
@Html.Partial("<Stuff>/TitlePage.cshtml", Model)
@Html.Partial("<Stuff>/Preamble.cshtml", Model)
@Html.Partial("<Stuff>/Contents.cshtml", Model)
@Html.Partial("<Stuff>/ExecSummary.cshtml", Model.SummaryData)
@Html.Partial("<Stuff>/ComparisonOptions.cshtml", Model.OtherData)
// x20

Working with the monolithic model is easy, but not very portable. For some other reports, you're constructing this giant object with dozens of null properties. And some reports don't use this object at all; all of their partial views take models of different types, or they're specific to that report & take totally different properties.

With the partials that require models of different types, it's a bit easier to follow what's going on. However, something like 50% of the partials want access to a "UserName" string or a "DesignName" or something similar, but there's little that these views actually have in common, other than that these duplicated properties end up in the text. So it feels like you're constructing a bunch of objects one after another with lots of similar properties. And sometimes they vary slightly in nomenclature, which can add a bit of confusion (e.g. "MainDesignTitle" vs "PrimaryDesignName").

There's a huge chunk of upfront processing to get calculation results that are utilized to generate graphs in a lot of the subsections & it's important that these, as well as the way things are named, are all consistent throughout a given report. So I was averse to re-implement them all as independent pages that use AJAX calls to populate themselves.

I want to have a consistent approach to these reports in future (at the moment it's a mix) & enable myself to slowly refactor the larger ones over time. I want to try and apply pragmatic design principles to this. I've thought about:

  • Having the monolithic model implement interfaces that line up with the types expected by the Razor views.

    • Then I can as the monolithic object & see the relevant properties at any time
    • There'd be no repetition of properties & would force unified property names across the partials
    • Each report would just need to implement the interfaces required by the partials.
    • But this feels like it could become very cluttered very quickly
    • Everything'd be stuck at this top level, and seems quite unintuitive to implement something like 25 different interfaces
  • Having each partial map to a property on a given report object

    • BigReportModel would for instance have ~25 different properties & pass the correct ones to the partial views when it's run
    • I'd try and create a constructor or mapper that, at the very least, handles all the duplication in one place
    • (Presently, for some of these properties, there's not a unified area where they're constructed, and so you end up with repetition of the repetition, and sometimes some reports give subtly different output)
    • The structure would be logical, as every report would just have some common / inherited report properties (e.g. Title) and then a property that represents each partial that's expected in that report

I think the current clutter made me apprehensive of the second option, but after typing this up, I think it makes more sense (favouring composition and all that). But I'd really like to know if there's a common design methodology for things like this.

Était-ce utile?

La solution

Re-composing a large view model into a bunch of sub view models is about the cleanest way you can do this. With view models, don't be afraid to duplicate data if it makes code easier to maintain. The performance bottleneck for most reports is the time it takes to query for data and crunch numbers. I wouldn't worry about any additional rendering time.

Keeping with a single view model paired to a single view is a way to tame large complicated views. This isn't a technical constraint. It is a philosophical constraint to make it easier to find views and view models.

Consider moving those partials into the Views/Shared/DisplayTemplates folder, and then using the @Html.DisplayFor(...) helper method. Doing this along with creating one property on your main view model for each partial can clean things up, and make things easier to find:

@model BigReportModel

@Html.DisplayFor(model => model.TitlePage)
@Html.DisplayFor(model => model.Preamble)
@Html.DisplayFor(model => model.ExecSummary)
@Html.DisplayFor(model => model.ComparisonOptions)
... and 20 more ...

Sure it makes the top level view model a giant list of properties, but you can push the initialization of the properties into the constructor, or simply pass the sub view models in as constructor arguments. Failing that, make the properties on the main view model a public setter if they can be null. This should reduce the number of properties in BigReportModel. Each sub view model might have its own constructor that takes just the data it needs:

var model = new BigReportModel(a, b, c, d)
{
    TitlePage = new ReportTitleModel("The report title", "Description"),
    Preamble = new ReportPreamble(pass, anything, it, needs),
    ExecSummary = new ReportExecutiveSummary(queryResultObject1,
                                             queryResultObject2,
                                             calculatedResult,
                                             ...)
};

If duplication of initialization logic is a concern and initializing these view models using constructor parameters gets out of hand, throwing together a "view model factory" isn't a bad way to go. It all comes back to the tried and true techniques for code reuse. Helper methods, factories, extension methods — any and all of these can help.

More info about display templates: Display and Editor Templates - ASP.NET MVC Demystified.

Licencié sous: CC-BY-SA avec attribution
scroll top