Is this HtmlHelper extension overkill? - asp.net-mvc

I've implemented a custom html helper extension method to render a partial view into my razor page and I'm concerned that the method I've used is "too heavy"...
I want to use a common spinner across my web project. The spinner I'm using requires a number of elements:
<div class="panel-spinner-container">
<div class="sk-spinner sk-spinner-cube-grid">
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
<div class="sk-cube"></div>
</div>
<div class="panel-spinner-msg"><p>loading...</p></div>
</div>
Rather than including this in razor code every time I want a spinner, I figured I'd toss it into an html helper method, so I could do this:
<div>
#Html.PanelSpinner()
</div>
I created a partial view for this which just contains the divs from above, and an html helper extension method:
public static MvcHtmlString PanelSpinner(this HtmlHelper html) {
var controllerContext = html.ViewContext.Controller.ControllerContext;
var viewData = html.ViewData;
var tempData = html.ViewContext.TempData;
using (var stringWriter = new System.IO.StringWriter()) {
var viewResult = ViewEngines.Engines.FindPartialView(controllerContext, "PanelSpinner");
var viewContext = new ViewContext(controllerContext, viewResult.View, viewData, tempData, stringWriter);
viewResult.View.Render(viewContext, stringWriter);
var stringResult = stringWriter.GetStringBuilder().ToString();
return new MvcHtmlString(stringResult);
}
}
This seems like an awful lot of processing required to render a spinner. What is the performance overhead on this type of method? Is it normal to use a helper for this type of scenario, or am I overkilling this? Is there a better (more performant) way to do this?
I very much like the ease of use but am concerned about performance (e.g., I could just use #Html.RenderPartial(...), but then I have to remember the name of the spinner partial, etc. The helper method wrapper is so much easier to use)

To render a partial view using an extension method, you can make use of the built in Partial() method of HtmlHelper. It can simply be
public static MvcHtmlString PanelSpinner(this HtmlHelper html)
{
return html.Partial("PanelSpinner");
}
Note: You need to include using System.Web.Mvc.Html;

Related

RenderAction() to make decoupled components but how to communicate with other components

I have a busy view where most of the sections on there work off an ID. I'm looking for a more component way to handle each section so I'm using RenderAction() for each section where they have their own controllers. However I have a search section/"component" and when they put in a new Id and submit on that section/"component", I need a way for that to communicate to all the other RenderActions() that new Id so they can do their thing (query DB to get more info specific to that section).
My Search section would be something like:
public class SearchController : Controller
{
[HttpGet]
public ActionResult SearchContract()
{
var vm = new SearchVM();
return PartialView(vm);
}
[HttpPost]
public ActionResult SearchContract(SearchVM Search)
{
return PartialView(Search);
}
}
#using (Html.BeginForm())
{
<div class="row">
<div class="col-md-3">Contract Id</div>
<div class="col-md-6">
#Html.TextBoxFor(m => m.Id, new { #class = "form-control" })
</div>
</div>
<input type="submit" />
}
Let's say ContractHeader is a section/"component" using RenderAction() that hits a different controller and method from the search:
public class ContractController : Controller
{
public ActionResult ContractHeader(int ContractId)
{
// query contracts
return PartialView(vm);
}
}
Again, I'm looking for a more component oriented way with this. Yes it could all be in one controller but that's not what I'm looking for here. I want a more decoupled/compartmentalized approach to these areas on my views but trying to figure out how they can communicate with each other when "events" happen.
I think I have it figured out. Basically on each search "component" (I'm calling components a separate controller and view that you use RenderAction() to get on your main view) the method that gets called when the search button is pressed will return the following code (I subclassed Controller and put tis method in)
public ActionResult RedirectWithQueryString()
{
// get the referrer url without the old query string (which will be the main view)
var uri = new Uri(Request.UrlReferrer.ToString());
var url = Request.UrlReferrer.ToString().Replace(uri.Query, "");
var allQS = System.Web.HttpUtility.ParseQueryString(uri.Query);
var currentQS = System.Web.HttpUtility.ParseQueryString(Request.Url.Query);
var combinedQS = new NameValueCollection();
// update existing values
foreach (var key in allQS.AllKeys)
{
combinedQS.Add(key, allQS[key]);
}
// add new values
foreach (var key in currentQS.AllKeys)
{
if (combinedQS.AllKeys.Contains(key))
combinedQS[key] = currentQS[key];
else
combinedQS.Add(key, currentQS[key]);
}
var finalUrl = url + combinedQS.ToQueryString();
return Redirect(finalUrl);
}
public class ContractSearchController : MyBaseController
{
// GET: ContractSearch
public ActionResult Index(ContractSearchVM model)
{
return PartialView("ContractSearch", model);
}
public ActionResult SearchContracts(ContractSearchVM model)
{
return RedirectWithQueryString();
}
}
public class StopsSearchController : MyBaseController
{
public ActionResult Index(StopsSearchVM model)
{
// query to get some search related reference data like states list for drop down
return PartialView("StopsSearch", model);
}
public ActionResult SearchStops(StopsSearchVM model)
{
return RedirectWithQueryString();
}
}
SearchContracts() and SearchStops() methods are called from their own forms in their own views using HttpGet. In those methods then we are provided with just that forms query string but we also can get the UrlReferrer query string which will have other search forms key/values in it. So RedirectWithQueryString() basically makes sure the final query string has ALL keys required to satisfy the model binding of any search components on the view and will update the given keys with the current value for the current search component that the submit button was on.
So this then causes us to refresh to the current view with all current key/values in query string for all search components which then is calling all the RenderActions() and the values can be passed.
#model FocusFridayComponents.Models.CombinedVM
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Index", "ContractSearch"); }
</div>
</div>
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Index", "StopsSearch"); }
</div>
</div>
</div>
<div class="col-md-6">
<!-- Contract Header -->
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Header", "ContractHeader", new { ContractId = Model.ContractSearch.Id }); }
</div>
</div>
<!-- Contract Routes -->
<div class="row">
<div class="col-md-12">
#* #{ Html.RenderAction("Index", "ContractRoutes", new { ContractId = Model.Id }); } *#
</div>
</div>
</div>
In the main view you're working on you just make a VM that combines the search VM's you're using on the view. The model binding will correctly map the query string keys that match the search VM's even when they are inside the combined VM. The catch here would be to make sure the keys/props of each search VM don't share the same names of any kind.
What's interesting is for the RenderAction() for contract and stops search I don't need to pass the model into it. The binding just does this automatically. For ContractHeader and ContractRoutes I am passing in a parameter because the idea is those are separate components and have their own input requirements and those can be named completely separate from any search models you may be using in your view so the binding wouldn't be able to map anything. This is a good thing though as it decouples your actual view components from your search components.
So you would do all of this to get components that are decoupled from each other but can still talk to each other and you can assemble your views and reuse a lot of these components by just gluing the RenderAction() parameters between them. This can help reduce giant monolithic VM's that tend to pop up on complex views you're making.

How to load partial razor page into razor view

I am trying to replace my view components with razor pages but it seems that it's not possible to load a partial razor page because a model is expected to be passed yet it is my understanding that the model for a razor page should be declared in the OnGetAsync method. Here is my code...
Razor Page
#page "{id:int}"
#model _BackgroundModel
<form method="POST">
<div>Name: <input asp-for="Description" /></div>
<input type="submit" />
</form>
Razor Page Code-Behind
public class _BackgroundModel : PageModel
{
private readonly IDataClient _dataClient;
public _BackgroundModel(IDataClient dataClient)
{
_dataClient = dataClient;
}
[BindProperty]
public BackgroundDataModel Background { get; set; }
public async Task OnGetAsync(int id)
{
Background = await _dataClient.GetBackground(id);
}
public async Task OnPostAsync()
{
if (ModelState.IsValid)
{
await _dataClient.PostBackground(Background);
}
}
}
Razor View
<div class="tab-pane fade" id="client-background-tab">
<div class="row">
<div class="col-sm-12">
#await Html.PartialAsync("/Pages/Client/_Background.cshtml", new { id = 1 })
</div>
</div>
</div>
Page Load Error
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type '<>f__AnonymousType0`1[System.Int32]',
but this ViewDataDictionary instance requires a model item of type
'WebApp.Pages.Client._BackgroundModel'
In this example (as per MS recommended approach in their docs) the model is set inside the OnGetAsync method which should be run when the page is requested. I have also tried #await Html.RenderPartialAsync("/Pages/Client/_Background.cshtml", new { id = 1 }) but the same error result.
How can I load the razor page into my existing view?
Microsoft confirmed this cannot be achieved and therefore razor pages cannot be used as a replacement for view components.
See the comments of their docs...
MS docs
#RickAndMSFT moderator15 hours ago
#OjM You can redirect to the page, or you can make the core view >code into a partial and call it from both.
Pages are not a replacement for partials or View Components.

ASP.NET MVC 3 diplay label + data in readonly scenario using templated HtmlHelpers

I need to display short description like label and near it data associated with this description.
For editing data it looks like
<div class="field">
#Html.LabelFor(m => m.CustomerTaxId)
#Html.EditorFor(m => m.CustomerTaxId)
</div>
Now I want to do the same thing for readonly data. I've written next code
<div>
#Html.LabelFor(m => m.TotalAmount)
#Html.DisplayFor(m => m.TotalAmount)
</div>
It seems working but it breaks semantic meaning of label tag which must be used for input tags only.
Is there a way in MVC 3 to get something like
<div>
<span class="label">Total amount</span>
<span class="value">1500.00 $</span>
</div>
with minimal efforts (think it can be done with heavy template usage)
It seems like I can overwrite template for Html.DisplayFor how to deal with Html.LabelFor?
You can create custom html helper as below,
public static MvcHtmlString Span<TModel, TValue>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData, null);
string spanInnerText = metadata.DisplayName ?? metadata.PropertyName;
TagBuilder tag = new TagBuilder("span");
tag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
tag.SetInnerText(spanInnerText);
return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal));
}
You could use in a view as,
#Html.Span(m => m.Name, new { #class = "label" })

binding inside html helper

i want to bind to the display style property of my Html.TextBox
<%= Html.TextBox("RunDates", Model.RunDates, new { style = "display: none" })%>
is this possible? i want to do something like the following that i know works for me:
<input id="btnBack" class="btnAction" type="button" value="Back" style="display: <%= Model.IsEnabledProductSetupBack?"inline":"none" %>;" />
or is there a way to post <input type="text" ... in mvc?
You could write a custom Html helper for this. There are 2 possibilities:
You replace this weakly typed TextBox helper with strongly typed TextBoxFor by taking advantage of a view model (which is what I would recommend) in which case you will be able to write your textbox like this:
<%= Html.MyTextBoxFor(x => x.RunDates) %>
You stick with weak typing in which case the best you could get is this:
<%= Html.TextBox("RunDates", Model.RunDates, Html.SelectStyle(Model.IsEnabledProductSetupBack)) %>
Now since I recommend the first solution, it is the one I would provide same code for:
public static class HtmlExtensions
{
public static IHtmlString MyTextBoxFor<TProperty>(
this HtmlHelper<MyViewModel> helper,
Expression<Func<MyViewModel, TProperty>> ex
)
{
var model = helper.ViewData.Model;
var htmlAttributes = new RouteValueDictionary();
htmlAttributes["style"] = "display: none;";
if (model.IsEnabledProductSetupBack)
{
htmlAttributes["style"] = "display: inline;";
}
return helper.TextBoxFor(ex, htmlAttributes);
}
}

asp.net mvc Ajax.BeginForm clone

I'm using asp.net mvc ajax.
The partial view is using Ajax.BeginForm (just an example):
<div id="divPlaceholder">
<% using (Ajax.BeginForm(new AjaxOptions { UpdateTargetId = "divPlaceholder" })) { %>
... asp.net mvc controls and validation messages
<input type="submit" value="Save" />
<% } %>
</div>
After update, if validation fails, the html is:
<div id="divPlaceholder">
<div id="divPlaceholder">
...form
</div>
</div>
I don't like that the returned html is inserted, instead it should replace original div.
Probably on POST I should not render <div> around form in partial view or render the div without id.
What else can I do in this situation?
I was thinking that maybe I should write a helper, something like Ajax.DivBeginForm, which will render form inside div on GET and hide the div on POST.
Can somebody provide a good advice how to write such helper (Ajax.DivBeginForm)?
I'd like it to work with using keyword:
<% using (Ajax.DivBeginForm(new AjaxOptions { UpdateTargetId = "myId" })) { ... }%>
My solution. Please comment if something is wrong.
public class DivMvcForm : MvcForm
{
private bool _disposed;
private MvcForm mvcForm;
private ViewContext viewContext;
public DivMvcForm(MvcForm mvcForm, ViewContext viewContext) : base(viewContext)
{
this.mvcForm = mvcForm;
this.viewContext = viewContext;
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
mvcForm.EndForm();
viewContext.Writer.Write("</div>");
}
}
}
Helper
public static class AjaxHelperExtensions
{
public static MvcForm DivBeginForm(this AjaxHelper ajaxHelper, AjaxOptions ajaxOptions)
{
var tagBuilder = new TagBuilder("div");
if (ajaxHelper.ViewContext.HttpContext.Request.RequestType == "GET"
&& string.IsNullOrWhiteSpace(ajaxOptions.UpdateTargetId) != true)
{
tagBuilder.MergeAttribute("id", ajaxOptions.UpdateTargetId);
}
ajaxHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
var theForm = ajaxHelper.BeginForm(ajaxOptions);
return new DivMvcForm(theForm, ajaxHelper.ViewContext);
}
}
And how it works
<% using (Ajax.DivBeginForm(new AjaxOptions { UpdateTargetId = "divPlaceholder" })) { %>
... controls
<% } %>
Result - when ModelState is invalid the partial view returns div without id.
I'm going to take a little different approach here, rather than getting your original solution to work, I'd recommend using the pattern normally followed in this scenario and not using a helper. I realize this is a bit later than the original post, but for future use by anyone : )
If your partial view has a form, then you will keep posting, and returning a form in a form in a form in a form, etc. so you want to have your PARENT contain BeginForm, the div, and renderpartial
using (Ajax.BeginForm("Index", "ProjectManager", new AjaxOptions() ....
<div id="divPlaceholder">
Html.RenderPartial(....)
</div>
If you want to encapsulate this logic in say, an "Order" partial view that is displayed on a Customer screen, then you have two options.
1. Include the BeginForm on the parent Customer view (which reduces code reusability as any view that wants to include the "Order" partial view must include the ajax wiring.
Or
2. You have two partial views for order. One is OrderIndex.ascx (or cshtml if razor) and one is OrderIndexDetail.ascx (or whatever naming convention you decide)
OrderIndex contains your Ajax.beginform and OrderIndexDetail has no form, only the partial view details.
Option 2 is more code (ok, literally about 30 more seconds of coding to move the ajax.beginform into another view) but increases code reusability.
You can handle submit form yourself:
<div id="divPlaceholder">
<% using (Html.BeginForm("action", "controller", FormMethod.Post, new { id = "submitForm"})) { %>
... asp.net mvc controls and validation messages
<input type="submit" value="Save" />
<% } %>
</div>
and write some javascript as:
$('#submitForm').submit(function() {
$.post('post-to-this-url',
data: { foo: formvalue1, bar: formvalue2},
function(data) {
// update html here
});
return false;
})

Resources