This is mostly boilerplate bootstrap paneling, with masonry on top.
How would you provide a generic razor to meet bootstrap/masonry without creating a bunch of classes or partials to account for special case(s)? How in razor would you write a method that can take inline markup (for example any @<text> call)?
One way is
@helper
Take note that HelperResult's documentation says specifically it is not intended to be used directly in your code.
So I headed in down the path of inline razor helper delegates:@{ var scope = (Web.ViewModels.ScopeModel)ViewBag.Scope; } @helper progressBar(int completion) { <div class="progress progress-striped"> <div class="progress-bar" role="progressbar" aria-valuenow="@completion.ToString()" aria-valuemax="100" aria-valuemin="0" style="width : @completion%;"> <span class="sr-only">@completion% Complete</span> </div> </div> } @helper widget(Func<dynamic,HelperResult> title,Func<dynamic,HelperResult> body,Func<dynamic, HelperResult> footer) { <div class="widget-wrapper md-widget"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">@title(null)</h3> </div> <div class="panel-body text-center"> @body(null) </div> <div class="panel-footer"> @footer(null) <div class="clearfix"></div> </div> </div> <div class="clearfix"></div> </div> } <div class="tile-layout"> <div class="row"> <div class="col-md-12"> <div class="widget-container"> @widget(@<text>Profiles</text>, @<text> <p>Your profile is @scope.Member.Completion% complete! To complete your profile click on the incomplete profiles below.</p> @progressBar(scope.Member.Completion) </text>, @<text> <a class="btn btn-default pull-right" href="#" role="button" data-toggle="modal" data-target="#pending-rewards-info">Learn more »</a> </text>) @foreach (var p in scope.Member.Profiles) { @widget(@<text> <img src="@p.Icon" /><a href="">@p.Name Profile</a><a href="#" class="btn" rel="tooltip" data-placement="bottom" title="" class="remaining" data-original-title="Number of Unanswered Questions for this Profile">@p.UnansweredCount</a> </text>, @<text> @progressBar(p.CompletionLevel) </text>,@<text> </text>) } </div> <div class="clearfix"></div> </div> </div> </div>holy cow that's a mess. To boot, formatting the document would actually break it, it would remove some tags and add others in inappropriate places.
calling @<text> is a way to pass markup as a delegate to a method so that you can provide parts and it can control where those go( or if they go).
So after a few hours of trying to improve it and find out what else I could do:
@{ Layout = MVC.Shared.Views._LayoutTiles; var scope = (Web.ViewModels.ScopeModel)ViewBag.Scope; } <div class="tile-layout"> <div class="row"> <div class="col-md-12"> <div class="widget-container"> @Html.Widget("lg", "panel-default panel-lead", @<h3 class="panel-title">Profiles</h3>, _ => profilesHeaderBody(scope.Member.Completion)) </div> <div class="widget-container"> @foreach (var p in scope.Member.Profiles) { @Html.Widget(addPanelClasses:"panel-default",header: _ => profileHeader(p),body: _ => profileBody(p)) } </div> <div class="clearfix"></div> </div> </div> </div> @helper profileHeader(Web.ViewModels.Interfaces.Profile p) { <h3 class="panel-title"> <img style="width:33px;height:24px;margin-top:-3px;" src="@p.Icon" /> <a href="@Url.Action(p.Name)">@p.Name Profile</a><span class="pull-right"><small><a href="#">Edit</a></small></span> </h3> } @helper profileBody(Web.ViewModels.Interfaces.Profile p) { @progressBar(p.CompletionLevel, "", "margin-bottom:0;") <span class="pull-left"><small>Completion: @p.CompletionLevel%</small></span> <span class="pull-right"><small>Missing answers: @p.UnansweredCount</small></span> } @helper profilesHeaderBody(int completion) { <p class="lead"><strong>Total completion: @completion%</strong></p> @progressBar(completion, "progress-bar-success", "max-width:500px;margin-left:auto;margin-right:auto;") }Still unwanted syntax elements, and we have added a small helper method, and a partial view.
@model CVS.Member.Web.ViewModels.Shared.WidgetViewModel <div class="widget-wrapper @Model.WidgetSize-widget" style="@Model.WidgetStyle"> <div class="panel @Model.AddPanelClasses"> @if (Model.HeaderMarkup != null) { <div class="panel-heading"> @Model.HeaderMarkup(null) </div> } @if (Model.Body != null) { <div class="panel-body text-center"> @Model.Body(null) </div> } @if (Model.UnwrappedContent!=null) { @Model.UnwrappedContent(null) } @if (Model.Footer != null) { <div class="panel-footer"> @Model.Footer(null) <div class="clearfix"></div> </div> } </div> <div class="clearfix"></div> </div>Every possible combination of a Func<> wound up with extra work in the calling site. Here's the model class:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.WebPages; namespace Web.ViewModels.Shared { public class WidgetViewModel { public string HeaderClass { get; set; } public string AddPanelClasses { get; set; } public string WidgetSize { get; set; } public Func<dynamic, HelperResult> HeaderMarkup { get; set; } public Func<dynamic, HelperResult> Body { get; set; } public Func<dynamic, HelperResult> UnwrappedContent { get; set; } public Func<dynamic, HelperResult> Footer { get; set; } public string WidgetStyle { get; set; } public WidgetViewModel() { WidgetSize = "md"; } } }So to call that with either inline helpers or inline markup:
<div class="tile-layout"> <div class="row"> <div class="col-md-12"> <div class="widget-container"> @Html.Widget("lg", "panel-default panel-lead", @<h3 class="panel-title">Profiles</h3>, _ => profilesHeaderBody(scope.Member.Completion)) </div> <div class="widget-container"> @foreach (var p in scope.Member.Profiles) { @Html.Widget(addPanelClasses:"panel-default",header: _ => profileHeader(p),body: _ => profileBody(p)) } </div> <div class="clearfix"></div> </div> </div> </div>See how I have to delegate the call to a delegate? and still haven't gotten around the problem of short sweet stuff for inline markup.
Perhaps we can eliminate that somehow!
public static Func<HelperResult> GetMarkupResult(this HtmlHelper helper, Func<dynamic, HelperResult> markup) { return ()=>markup(null); }
Solution
Finally what broke the linguistics barrier... looking at HelperResult and seeing that it already implements IHtmlString. I was concerned that it was going to use ToString and re-encode the markup. Reworked the helper and model.public static IHtmlString GetMarkupResult(this HtmlHelper helper, Func<object, HelperResult> markup) { return markup(null); }
Web.ViewModels.Shared { public class WidgetViewModel { public WidgetViewModel(string headerText = null) { WidgetSize = "md"; if (headerText.IsNullOrEmpty() == false) HeaderMarkup = new MvcHtmlString(headerText); } public string HeaderClass { get; set; } public string AddPanelClasses { get; set; } public string WidgetSize { get; set; } public IHtmlString HeaderMarkup { get; set; } public IHtmlString Body { get; set; } public IHtmlString UnwrappedContent { get; set; } public IHtmlString Footer { get; set; } public string WidgetStyle { get; set; } } }Here's the partial, it is cleaner as well
<div class="widget-wrapper @Model.WidgetSize-widget" style="@Model.WidgetStyle"> <div class="panel panel-default @Model.AddPanelClasses"> @if (Model.HeaderMarkup != null) { <div class="panel-heading"> @Model.HeaderMarkup </div> } @if (Model.Body != null) { <div class="panel-body text-center"> @Model.Body </div> } @if (Model.UnwrappedContent!=null) { @Model.UnwrappedContent } @if (Model.Footer != null) { <div class="panel-footer"> @Model.Footer <div class="clearfix"></div> </div> } </div> <div class="clearfix"></div> </div>A cleaner result for the caller and an improved api for the system:
<div class="tile-layout"> <div class="row"> <div class="col-md-12"> <div class="widget-container"> @Html.Widget(new WidgetViewModel { WidgetSize = "lg", AddPanelClasses = "panel-default panel-lead", HeaderMarkup = Html.GetMarkupResult(@<h3 class="panel-title">Profiles</h3>), Body = profilesHeaderBody(scope.Member.Completion) }) </div> <div class="widget-container"> @foreach (var p in scope.Member.Profiles) { @Html.Widget(new WidgetViewModel { HeaderMarkup =profileHeader(p), Body = profileBody(p) }) } </div> <div class="clearfix"></div> </div> </div> </div>
No comments:
Post a Comment