There has been many discussion on ASP.NET MVC and Codebehind-files, mostly where it has been pointed out that these Codebehind-files are evil.
So my question is, how do you handle page-specific logic?
What we don't want here is spaghetti-code in the inline-code and we don't want page-specific code scattered throughout helper-classes or on top of the HTML-helper-class.
An example would be:
<% for(int i = 0; i < companyList.Count; i++) { %>
RenderCompanyNameWithRightCapsIfNotEmpty(company, i)
<% } %>
With accompanying codebehind:
private string RenderCompanyNameWithRightCapsIfNotEmpty(string company, index)
{
if (index == 0) {
return string.Format("<div class=\"first\">{0}</div>", company);
}
// Add more conditional code here
// - page specific HTML, like render a certain icon
string divClass = (index % 2 == 0) ? "normal" : "alternate";
return string.Format("<div class=\"{1}\">{0}</div>", company, divClass);
}
This will only be used on one page and is most likely subject to change.
Update: A couple approaches I thought about where these:
1) Inline codebehind on page - with simple methods that returns strings.
<script runat="server">
private string RenderCompanyHtml(string companyName) ...
<script>
2) Putting a method which returns a string in the Controller. But that would be putting View-logic into the Controller.
public class SomeController : Controller
{
[NonAction]
private static string RenderCompanyHtml(string companyName) ...
public ActionResult Index() ...
}
You should put that code in the controlleraction where you prepare the viewdata.
I usually make a region "helper methods" in my controller class with a few [NonAction] methods to keep things clean.
So my (simplified) controller would look like this:
public class SomeController : Controller
{
#region Helper methods
[NonAction]
private static string CompanyNameWithRightCapsIfNotEmpty(string company)
{
if (string.IsNullOrEmpty(company)) {
return company;
}
return UpperCaseSpecificWords(company);
}
#endregion
public ActionResult Companies()
{
var companies = GetCompanies();
var companyNames = companies.Select(c => CompanyNameWithRightCapsIfNotEmpty(c.Name));
ViewData["companyNames"] = companyNames;
return view();
}
}
Helper methods are one good way of handling page specific code, but I think it is a;ways preferable to get your model to show the data you need.
If you're going to go for the helper option, you would be better served by making the operation it perfroms a bit less page specific. If your method RenderCompanyNameWithRightCapsIfNotEmpty has to be so specific it would be better if your model provided it. One way would be to have the model provide a list with the text already formatted, and expose it as a public property (say an IEnumerable of formatted company names).
Use Html Helpers.
Like so create the helper methods in a static class:
public static string Label(this HtmlHelper helper, string target, string text)
{
return String.Format("<label for='{0}'>{1}</label>", target, text);
}
.. then use in your view:
<span><% =Html.Label("FinishDateTime.LocalDatetime", "Finish Time:")%><br />
You could create a helper method called maybe RenderCompanyName(string[] companies) that checked for nulls, did the caps manipulation and rendered the html in between - all in the same helper if you like.
Also: controller action methods should be light - ie. only getting the data and returning views. You should delegate things like manipulation of data for presentation to views and Html helpers.
EDIT: Here is a helper that you might be after:
This helper renders an IList<> to html in the form of an unordered list <ul>...</ul>. The useful thing about it is that it gives you control over how the list is rendered thru css AND it allows you to render additional html/content for each item. Take a look - this is the helper:
public static string UnorderedList<TItem>(this HtmlHelper helper,
IList<TItem> items, Func<TItem, string> renderItemHtml,
string ulID, string ulClass, string liClass)
{
StringBuilder sb = new StringBuilder();
// header
if (!ulID.IsNullOrTrimEmpty()) sb.AppendFormat("<ul id='{0}'", helper.Encode(ulID.Trim()));
else sb.AppendFormat("<ul");
if (!ulClass.IsNullOrTrimEmpty()) sb.AppendFormat(" class='{0}'>", helper.Encode(ulClass.Trim()));
else sb.AppendFormat(">");
// items
foreach (TItem i in items)
{
if (!liClass.IsNullOrTrimEmpty())
sb.AppendFormat("<li class='{0}'>{1}</li>", helper.Encode(liClass.Trim()),
renderItemHtml(i));
else
sb.AppendFormat("<li>{0}</li>", renderItemHtml(i));
}
// footer
sb.AppendFormat("</ul>");
return sb.ToString();
}
..using it is easy. here is a simple example to render a list of tags:
<div id="tags">
<h2>Tags</h2>
<%=Html.UnorderedList<Tag>(Model.Tags.Tags,tag=>
{
return tag.Name;
},null,null,null) %>
</div>
..you can see in my usage example that i have chosen not to specify any css or id attribute and i simply return the name of the Tag item thru the use of the anonymous delegate. Anonymous delegates are way easy to use.. in your case maybe something like this would work:
<div id="tags">
<h2>Tags</h2>
<%=Html.UnorderedList<string>(ViewData["companies"],company=>
{
if (someCondition) return company.ToUpper();
else return company;
},null,null,null) %>
</div>
.. ViewData["companies"] is an IList<string> for simplicity.
Related
In my _layout.cshtml page, I've got some elements that need to be hidden on some pages. I know the pages on which we won't display some parts. For a single page, I could just do this:
#if (ViewContext.RouteData.Values["action"].ToString() != "LogIn") {
<div> .... <div>
}
But that gets messy and long with multiple pages. Someplace, ideally not in the _Layout page, I could build a list of actions, and if the current action is any of them, set a boolean variable (ShowStuff) to false. Then just do this on _Layout:
#if (ShowStuff== true) {
<div> .... <div>
}
I'm just not sure where would be the best-practice way to examine that list of actions and set the boolean. Can the _Layout page have it's own model and controller like a normal view?
Similarly to MikeSW answer, I'd use an action filter, but I would populate ViewData with a specific ViewModel. When you want to display it simply DisplayFor the value, if it's populated the template is used by whatever type the model is, if it's null nothing is displayed. (examples below from memory, may not be exactly correct.)
public BlahModelAttribute : ActionFilterAttribute
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
BlahModel model = Db.GetModel();
filterContext.Controller.ViewData.Set(model);
}
}
ViewData extensions:
public static ViewDataExtensions
{
private static string GetName<T>()
: where T : class
{
return typeof(T).FullName;
}
public static void Set<T>(this ViewDataDictionary viewData, T value)
: where T : class
{
var name = GetName<T>();
viewData[name] = value;
}
public static T Get<T>(this ViewDataDictionary viewData)
: where T : class
{
var name = GetName<T>();
return viewData[name] as T;
}
}
In your view:
#{var blahModel = ViewData.Get<BlahModel>() }
#Html.DisplayFor(m => blahModel)
If devs would stop looking for the 'best way' for every problem they have, that would be great. No best way here, just opinionated solutions. Here's mine: You can create an action filter [ShowNav] and decorate any controller/action you need. That filter will put a boolean into HttpContext.Items . Create then a HtmlHelper which checks for the boolean. Then in _layout, if (Html.CanShowNavig()) { <nav> } . That's the easiest solution that comes to my mind.
<div>
Email:
<a id="email href="mailto:#Model.Contact.Email">#Model.Contact.Email.ToStringMyCustomFormatted</a>
</div>
I want to create custom method for formatting and apply it like this.
(in the same way we can apply ToString() method to this.)
I don't want to use JavaScript to do any formatting by using Document.Ready().
In short I want to extend ToString method something like ToStringMyCustomFormatted, by which I can apply my own rules to string output.
I am not even sure if something like can be done.
Please Enlighten !
You could use a simple C# extension method (or directly on your Contact model):
public static class YourContactModelExtensions {
public static string ToStringMyCustomFormat(this YourContactModel m) {
// TODO
}
}
or if you wanted it to apply to all strings:
public static class StringExtensions {
public static string ToMyCustomFormat(this string s) {
// TODO
}
}
or a Razor helper function on your Razor page:
#helper ToStringMyCustomFormat(YourContactModel m) {
#* TODO *#
}
or
#helper ToMyCustomFormat(string s) {
#* TODO *#
}
I have some Customer Details and I only want to show fields which have a value.
For example if Telephone is null don't show it.
I currently have in my view model
public string FormattedTelephone
{
get { return string.IsNullOrEmpty(this.Telephone) ? " " : this.Telephone; }
}
And in my view
#Html.DisplayFor(model => model.FormattedTelephone)
This is working correctly, however, I would like to show the Field Name if the field has a value e.g.
Telephone: 02890777654
If I use #Html.DisplayNameFor in my view it shows the field name even if the field is null.
I also want to style the field name in bold and unsure of where I style it - the view or the view model.
For the bold style you can use this bit of code in your view, but of course it's proper to use an external style sheet.
<style type="text/css">
.telephone{
font-weight: bold;
}
</style>
You can do the check for null in your view and conditionally display the data:
#if (Model.FomattedTelephone != null)
{
<div class="telephone">
#Html.DisplayFor(model => model.FormattedTelephone)</div>
}
For style add a class for to the span you can put around field name.
You could create your own HtmlHelper that will only write if string is not null or empty.
Or you could add a DisplayTemplates something like here:
How do I create a MVC Razor template for DisplayFor()
For more background on helpers in razor read the following
http://weblogs.asp.net/scottgu/archive/2011/05/12/asp-net-mvc-3-and-the-helper-syntax-within-razor.aspx
And if they're in your App_Code folder read the answer to this
Using MVC HtmlHelper extensions from Razor declarative views
You'll probably want to over the default helper page with this (and inherit in your helper classes in App_Code)
public class WorkaroundHelperPage : HelperPage
{
// Workaround - exposes the MVC HtmlHelper instead of the normal helper
public static new HtmlHelper Html
{
get { return ((WebViewPage)WebPageContext.Current.Page).Html; }
}
public static UrlHelper Url
{
get { return ((WebViewPage) WebPageContext.Current.Page).Url; }
}
}
I would make a helper for this, something like this:
using System.Web.Mvc.Html;
public static class HtmlHelpers
{
public static MvcHtmlString LabelDisplayFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
{
StringBuilder html = new StringBuilder();
string disp = helper.DisplayFor(expression).ToString();
if (!string.IsNullOrWhiteSpace(disp))
{
html.AppendLine(helper.DisplayNameFor(expression).ToString());
html.AppendLine(disp);
}
return MvcHtmlString.Create(html.ToString());
}
}
Now, when you are in your View, you can simply do this (given you include the namespace in your view or web.config):
#Html.LabelDisplayFor(model => model.FormattedTelephone)
All it really does is check to see if your display helper is not an empty string, if it is, it will simply append your LabelFor and DisplayFor, if not, it will return an empty string.
I usually prefer to use Display/Editor Templates instead of HtmlHelper. Here is template that I have used to perform exactly the same task, its designed for bootstrap data list but anyone can adjust it easily.
#if (Model == null)
{
#ViewData.ModelMetadata.NullDisplayText
}
else if (ViewData.TemplateInfo.TemplateDepth > 1)
{
#ViewData.ModelMetadata.SimpleDisplayText
}
else
{
<dl class="dl-horizontal">
#foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm)))
{
if(MvcHtmlString.IsNullOrEmpty(Html.Display(prop.PropertyName)))
{
continue;
}
if (prop.HideSurroundingHtml)
{
#Html.Display(prop.PropertyName)
}
else
{
<dt>#prop.GetDisplayName()</dt>
<dd>#Html.Display(prop.PropertyName)</dd>
}
}
</dl>
}
Key line is:
if(MvcHtmlString.IsNullOrEmpty(Html.Display(prop.PropertyName)))
Its based on object template so to use it you need use it on object or whole model like
#Html.DisplayForModel("TemplateName")
I'm trying to make a custom template for a basket item list. I need a few different templates, as I have different ways of displaying the item, depending on if it's on the webpage or in a mail. Now my problem is, that when I use the default name it works flawlessly.
#Html.DisplayFor(b => b.Items)
But when I try to add a template name, I get an expection that my templates needs to be of a list type IEnumerable and not BasketItem.
#Html.DisplayFor(i => basket.Items, "CustomerItemBaseList")
Any ideas where my mistake is, or why it's not possible are appreciated. Thanks.
Unfortunately that's a limitation of templated helpers. If you specify a template name for a collection property the template no longer applies automatically for each item of the collection. Possible workaround:
#for (int i = 0; i < Model.Items.Length; i++)
{
#Html.DisplayFor(x => x.Items[i], "CustomerItemBaseList")
}
That's a good idea, Darin. I'm lazy though, so I'd like to take it one step further and make an actual helper that wraps this. I also took out the lambda expression to simplify it for my case, but you can easily add that functionality back in.
public static class DisplayTextListExtension
{
public static MvcHtmlString DisplayForList<TModel>(this HtmlHelper<TModel> html, IEnumerable<string> model, string templateName)
{
var tempResult = new StringBuilder();
foreach (var item in model)
{
tempResult.Append(html.DisplayFor(m => item, templateName));
}
return MvcHtmlString.Create(tempResult.ToString());
}
}
Then the actual usage looks like:
#Html.DisplayForList(Model.Organizations, "infoBtn")
I liked Dan's answer but just adjusted slightly as it can work for any IEnumerable:
using System.Collections;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace YourProject.Whatever
{
public static class DisplayExtensions
{
public static MvcHtmlString DisplayForIEnumerable<TModel>(this HtmlHelper<TModel> html, IEnumerable model, string templateName)
{
var tempResult = new StringBuilder();
foreach (var item in model)
{
var item1 = item;
tempResult.Append(html.DisplayFor(m => item1, templateName));
}
return MvcHtmlString.Create(tempResult.ToString());
}
}
}
And of course:
#Html.DisplayForIEnumerable(Model.Organizations, "NameOfYourDisplayTemplate")
I suggest another solution useful even more with lists of heterogeneous objects (i.e. BasketItem subclasses), using the additionalViewData parameter of the DisplayFor method, like:
#DisplayFor(b=>b.Items, new { layout="row" })
in this way the helper works fine with IEnumerable<T>, calling for each item (subclass of T) the relative DisplayTemplate, passing it the additionalViewData values in the ViewData dictionary.
The template could so output different code for different layout values.
In the example above the template named View\Shared\DisplayTemplates\BasketItem (or the name of the subclass) should be like this:
#model MyProject.BasketItem // or BasketItem subclass
#{
string layout = ViewData["layout"] as string ?? "default";
switch(layout)
{
case "row":
<div class="row">
...
</div>
break;
// other layouts
...
default: // strongly recommended a default case
<div class="default-view>
...
</div>
break;
}
}
It is strongly recommended to provide always a default code.
I hope this suggestion could help.
Coming from the asp.net background, I really appreciated the concept of 'validationGroup' when adding validation to a page. I've been searching for a corresponding concept within mvc.net and haven't had much luck.
Is this concept available in mvc.net? If not, what alternatives do I have?
Unfortunately no, it doesn't come with anything like that.
I blogged about a workaround a wee while back.
ASP.NET MVC - Validation Summary with 2 Forms & 1 View
The jist of the blog post:
namespace System.Web.Mvc
{
public static class HtmlExtensions
{
public static string ActionValidationSummary(this HtmlHelper html, string action)
{
string currentAction = html.ViewContext.RouteData.Values["action"].ToString();
if (currentAction.ToLower() == action.ToLower())
return html.ValidationSummary();
return string.Empty;
}
}
}
And
<h2>Register</h2>
<%= Html.ActionValidationSummary("Register") %>
<form method="post" id="register-form" action="<%= Html.AttributeEncode(Url.Action("Register")) %>">
... blah ...
</form>
<h2>User Login</h2>
<%= Html.ActionValidationSummary("LogIn") %>
<form method="post" id="login-form" action="<%= Html.AttributeEncode(Url.Action("LogIn")) %>">
... blah ...
</form>
HTHs,
Charles
Expanding on Charlino's answer, and including HtmlAttributes and other ValidationSummary properties:
public static MvcHtmlString ActionValidationSummary(this HtmlHelper html, string action, bool excludePropertyErrors, string message, object htmlAttributes = null)
{
var currentAction = html.ViewContext.RouteData.Values["action"].ToString();
if (currentAction.ToLower() == action.ToLower())
{
return html.ValidationSummary(excludePropertyErrors, message, htmlAttributes);
}
return new MvcHtmlString(string.Empty);
}
Charles's method was the only approach I could find that actually worked for my purposes!
(I.e. two forms on one MVC page -> without doing forms inside partials and ajax loads for the partials. This was no good for me, as I wanted to return differing result sets to be rendered outside the form div, depending on which form was submitted)
I would advise a slight modification to the Html Extension though, because you still want a validation summary to be rendered for the non-matched validation summary so that client side validation works:
namespace System.Web.Mvc
{
public static class HtmlExtensions
{
public static MvcHtmlString ActionValidationSummary(this HtmlHelper html, string action)
{
string currentAction = html.ViewContext.RouteData.Values["action"].ToString();
if (currentAction.ToLower() == action.ToLower())
return html.ValidationSummary();
return new MvcHtmlString("<div class=\"validation-summary-valid\" data-valmsg-summary=\"true\"><ul><li style=\"display:none\"></li></ul></div>");
}
}
}