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