Custom type-dependent template loader - c#

As you might know, ASP.NET MVC has support for custom view overrides for model fields within views. There are special folders in the Views folder called Views\Shared\EditorTemplates, Views\Shared\DisplayTemplates and so on, and these folders can contain files like Views\Shared\EditorTemplates\String.cshtml, which will override the default view used when calling #Html.EditorFor in a view with a model with a String field.
What I want to do is to use this functionality for a custom kind of templates. I want to have a folder like Views\Shared\GroupTemplates that may contain e.g. Views\Shared\GroupTemplates\String.cshtml and Views\Shared\GroupTemplates\Object.cshtml, and I want to create a HtmlHelper method that allows me to call for example Html.GroupFor(foo => foo.Bar), which will load the template in String.cshtml if Bar is a String property, and the template in Object.cshtml otherwise.
Full example of the expected behavior; if Views\Shared\GroupTemplates\String.cshtml contains this:
#model String
This is the string template
... and Views\Shared\GroupTemplates\Object.cshtml contains this:
#model Object
This is the object template
I have a model like:
class Foo
{
public bool Bar { get; set; }
public String Baz { get; set; }
}
And a view in Views\Foo\Create.cshtml like:
#model Foo
#Html.GroupFor(m => m.Bar)
#Html.GroupFor(m => m.Baz)
When I render the view Create.cshtml, the result should be this:
This is the object template
This is the string template
How should GroupFor be implemented?

The thing is that you can easily specify your view location like that
html.Partial("~/Views/Shared/GroupTemplates/YourViewName.cshtml");
or even override default behaviour by implementing custom view engine, for an example see this blog A Custom View Engine with Dynamic View Location
But you also want to reuse the logic which determines the view name based on its model type. So that if a view with String name doesn't exist an Object view is picked up. Which means going through parent classes.
I've had a look how EditorFor is implemented:
public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
return html.TemplateFor<TModel, TValue>(expression, null, null, DataBoundControlMode.Edit, null);
}
It uses TemplateFor method which is internal and you can't just reuse it.
So I can only see of two options:
Implement you custom logic by checking if a view file with a correct name exists by trying model type name and its parent classes. And if you find a proper view just use Partial extension in your helper.
Try to use reflection to call internal method. But this approach is more like a hack than a solution.
Hope it helps!

Related

Set a default value to the model being passed to Partial View

I have a partial view that is called from another partial view (kind of nested partial views).
The outer partial view is called Company and the inner partial view is a custom control called searchHelp. Both accept a parameter.
Now the Company view gets a parameter of type company and searchHelper accepts an optional string. This part works fine as I am testing the model value for null and assigning is default text as #((Model==null)?"Enter Text":Model) when used in other views even without passing a parameter.
In my case of nested views, if I dont provide a string as model for searchHelper then it takes company as model from the outer view i.e company and gives an error.
The #model definition is not a value setter, it's merely telling Razor what type of view to instantiate. You can't define a default value here. If you don't pass a model to your partial, then it will use the model of the parent view, which is Company in this case. Company is not a string, obviously, so you get that error. If you want to pass a default value for the partial, do that in the second parameter to Html.Partial:
#Html.Partial("searchHelp", Model.SomeStringProperty ?? "Enter Text")
You can assign a default value to the string-as-model from where it's called in the view:
//null coalesce to default string value:
#Html.Partial("searchHelp", Model.searchHelp ?? "default value")
...though you might do better using an htmlhelper, where you can define the default value just one time:
public IHtmlString SearchHelp(this HtmlHelper html, string searchHelp = "default value")
{
// make html here
}
Then
#Html.SearchHelp(Model.searchHelp);

How do you put a ViewBag item into a Text Box?

I have the following code and I get an error saying:
has no applicable method named 'TextBoxFor' but appears to have an extension method by that name.
My Code:
#Html.TextBoxFor(ViewBag.taglist)
Why don't you use strongly typed model in your view instead of ViewBag. This will make your life easier.
In fact, you must use a model to with TextBoxFor, otherwise it just won't work. See the definition of TextBoxFor - as a second parameter it takes a lambda expression that takes a property form a model.
If you want just a text box, two options:
#Html.TextBox("NameOfTheTextbox", (String)ViewBag.SomeValue)
or just go
<input type="text" value="#ViewBag.SomeValue" />
No complex solutions required.
I agree with other suggestions of using a strongly-typed model, because the compile-time error support is so much better than debugging exceptions. Having said that, in order to do what you want, you can use this:
#Html.TextBox("NameOfTextBox", (string)ViewBag.taglist)
Update: A Simple Example
Now that you've provided some details in your comments, I've taken a guess at what you might be doing, in order to provide a simple example.
I'm assuming you have a list of tags (like SO has per question) that you'd like to display neatly in a textbox, with each tag separated by a space. I'm going to assume your Tag domain model looks something like this:
public class Tag
{
public int Id { get; set; }
public string Description { get; set; }
}
Now, your view will need a list of the tags but will likely need some other information to be displayed as well. However, let's just focus on the tags. Below is a view model to represent all the tags, taking into account that you want to display them as a string inside a textbox:
public class SomeViewModel
{
public string Tags { get; set; }
// Other properties
}
In order to get the data you want you could grab all of the tags like this:
public ActionResult Index()
{
using (YourContext db = new YourContext())
{
var model = new SomeViewModel();
model.Tags = string.Join(" ", db.Tags.Select(t => t.Description).ToList());
return View(model);
}
}
Notice how I'm directly passing model to the view.
The view is now very simple:
#model SomeViewModel
#Html.EditorFor(m => m.Tags)
The model directive is what signifies that a view is strongly-typed. That means this view will expect to receive an instance of SomeViewModel. As you can see from my action code above, we will be providing this view the type that it wants. This now allows us to make use of the strongly-typed HtmlHelper (i.e. Html.XxxFor) methods.
In this particular case, I've used Html.EditorFor, as it will choose an appropriate input element to render the data with. (In this case, because Description is a string, it will render a textbox.)
You cannot use Html.TextBoxFor without explicitly setting a type for your model within the view. If you don't specify a type it defaults to dynamic. If you want to do model binding then you must use an explicit type rather than a dynamic type like ViewBag. To use Html.TextBoxFor you must define a model type that defines the property that you wish to bind. Otherwise you have to use Html.TextBox and set the value manually from ViewBag. As others have said, you will make your life much easier if you use a statically typed model and take advantage of the inbuilt MVC model binding.
You have to use a lambda expression to select the property, plus you will have to cast the ViewBag member to the correct type.
#Html.TextBoxFor(model => (string)ViewBag.taglist)

ASP.NET MVC 3 Razor DisplayFor Delegate

I'm getting this error:
Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
Here's my code (custom HTML helper, wrapping DisplayFor so i can choose a template):
public static string DisplayLocationTypeFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, LocationType>> expression, bool plural = false)
{
return plural ?
htmlHelper.DisplayFor(expression, "LocationTypePlural").ToHtmlString() :
htmlHelper.DisplayFor(expression).ToHtmlString();
}
When i use it like this, it works:
#Html.DisplayLocationTypeFor(model => model.LocationType)
Because the model has a property for LocationType.
But when i do this in another custom HTML helper:
public static MvcHtmlString SearchPreferenceButtonForModel<TModel>(this HtmlHelper<TModel> htmlHelper)
{
// .. other code
foreach (var property in htmlHelper.ViewData.ModelMetadata.Properties)
{
if (property.PropertyName == "LocationType")
htmlHelper.DisplayLocationTypeFor(model => ((LocationType)Enum.ToObject(typeof(LocationType), property.Model)), true);
}
}
It errors.
I can change my DisplayLocationTypeFor helper to use htmlHelper.Display instead, but i'm not sure how.
Any ideas?
What i'm trying to do, is that i have a specific way of rendering out the LocationType model, that i want to happen across the site. Internally, the template uses a resource file, and some other smarts based on the URL. In other words, there is logic - which i don't wanted repeated.
This way, all my views/templates call into this template as a standard way of rendering the LocationType.
You need to read the error message:
Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
It's telling you that only certain types of (very simple!) lambda expressions are permitted in a Razor template. If you have something more complex, you need to compute the value before you try to pass it to the template. Something like this should work:
if (property.PropertyName == "LocationType") {
LocationType locationType = (LocationType) Enum.ToObject(typeof(LocationType), property.Model));
htmlHelper.DisplayLocationTypeFor(model => locationType, true);
}
You can achieve that by composing a display template for LocationType model.
Here is an answer that says how to achieve that. In short:
Create a folder ~/Views/Shared/DisplayTemplates.
Create a view named LocationType in the new folder you created with model type LocationType.
Whenever you try a #DisplayFor(model => model.LocationType), the view you created for LocationType will be rendered.

Reducing code duplication using HTML helpers

I have a fairly simple data audit web application written with ASP MVC which effectively has two views of the same model for different purposes.
Agent view - The form filled out by the person validating information the information. Each field on the form in this view has 3 subfields:
a. Original Value - The value from the database before the call was made
b. New Value - The value provided by the person on the phone if it differs from the original.
c. Action - A general indication of what happened
QC View - The form filled out by someone who reviews the work performed in Agent view. Each field on the form in this view has 5 subfields:
a. Original Value - Same as above
b. Agent Value - The value provided in 1b above by the agent.
c. QC Value - The corrected "New Value" if the value specified by the agent is incorrect.
d. Agent Action - Same as above, except read only in this view
e. QC Action - The corrected "New Action" if improperly chosen by the agent.
The only differences between the two views are the subfields available. I'd like to be able to use a single view to represent both views since the overall structure of the pages is identical, and just use HTML helpers to handle the differences in subfields. What I have so far are 2 distinctly separate series of helpers (currently in the same class though could be separated):
// Agent controls
public static MvcHtmlString AuditControl(this HtmlHelper htmlHelper, string id, string fieldLabel, MvcHtmlString editControl, string cssClass)
public static MvcHtmlString AuditControl(this HtmlHelper htmlHelper, string id, string fieldLabel, string editControl, string cssClass)
public static MvcHtmlString AuditControl<COMPLEX>(this HtmlHelper htmlHelper, string id, string fieldLabel, string cssClass) where COMPLEX : AbstractComplex, new()
// QC controls
public static MvcHtmlString ReviewControl(this HtmlHelper htmlHelper, string id, string fieldLabel, MvcHtmlString editControl, string cssClass)
public static MvcHtmlString ReviewControl(this HtmlHelper htmlHelper, string id, string fieldLabel, string editControl, string cssClass)
public static MvcHtmlString ReviewControl<COMPLEX>(this HtmlHelper htmlHelper, string id, string fieldLabel, string cssClass) where COMPLEX : AbstractComplex, new()
Where the third implementations handle the more complex fields composed of multiple pieces of data (like Full name, Address, etc).
One possible solution that I've considered is to separate the different types of controls into different classes which implement a common interface and then pass them as type parameters to more generic HTML helpers. I think this would work but then I'd somehow need to be able to tell the view which implementation it should use to draw the view, which seems problematic because it seems to blur the line between View and Controller.
One less appealing approach that seems obvious is to pass a sort of admin flag from the controller which would be used by a generic (in logic not meaning type generic) factory helper and build the logic in it to know which series of methods to use. This would keep the model and view separate, but feels dirty because then the HTML helper would become responsible for more than just building the HTML.
Is this a reasonable situation to break the separation of concerns as designed by MVC or is there a more appropriate solution?
Since you are using MVC3, I would recommend using child actions for the sub fields:
http://haacked.com/archive/2009/11/18/aspnetmvc2-render-action.aspx
Child actions allow you to execute an action on a controller inside of your view, this would be a much cleaner approach.
I was able to implement (my interpretation of) the advice provided by #SoWeLie fairly simply. It involved creating a new Model to house a superset of the possible control properties and a new view to be drawn for each different control set (one for Audit, and one for Review). The problem with it was the resulting View API was ugly:
#Html.RenderAction("DrawControl", new { id = "ID" ... })
// Repeated for all of the overloads of DrawControl
and each Controller action contained something like:
public ActionResult DrawControl(string id, ...)
{
// FieldControl being the name of my Model
var viewModel = new FieldControl() { ID = id, ... };
if (shouldRenderAudit)
return PartialView("AuditControl", viewModel);
else
return PartialView("ReviewControl", viewModel);
I couldn't figure out how to get my generic helper to work in this scenario, and besides, I wanted to remove reduce obvious code duplication so this quickly became:
#functions {
public string DrawControl(string id, ...)
{
return Html.Render("DrawControl", new { id = "ID" });
}
// Repeated for all of the overloads of DrawControl
}
#DrawControl("ID", ...)
With the same controller action. The problem with this (ignoring the fact that the View had functions at all) was that the #functions block had to be included in any view that wanted the benefit of using them (which is currently only 2 but will soon enough balloon to 5 and who knows what my predecessor is going to do with this). I quickly reworked the code again, this time to bring back the helpers (generally keeping the views, model, and controller changes) and finally ended up with this:
View:
#(Html.DrawComplexControl<ProviderName>("id", ...))
#Html.DrawSimpleControl("id", ...)
Controller:
// One common action that is used to determine which control should be drawn
public ActionResult DrawControl(FieldControl model)
{
if (shouldRenderAudit)
return PartialView("AuditControl", model);
else
return PartialView("ReviewControl", model);
}
Helper:
public static MvcHtmlString DrawControl(this HtmlHelper htmlHelper, string id, ...)
{
var model = new FieldControl() { ID = id, ... };
return htmlHelper.Action("DrawControl", model);
}
public static MvcHtmlString DrawSimpleControl(this HtmlHelper htmlHelper, string id, ...)
{
return DrawSimpleControl(htmlHelper, id, ...);
}
public static MvcHtmlString DrawSimpleControl(this HtmlHelper htmlHelper, string id, ...)
{
// Set some defaults to simplify the API
return DrawControl(htmlHelper, id, ...);
}
public static MvcHtmlString DrawComplexControl<T>(this HtmlHelper htmlHelper, string id, ...) where T : AbstractComplex, new()
{
// Build the required controls based on `T`
return DrawControl(htmlHelper, id, ...);
}
Of course there were about a half dozen other iterations between the ones shown to help the situation, and none of them did to the extent necessary. I'm sure there are improvements to be made yet, but this is what I have so far.
Doing it this way provides a very simple API for the View to use without it having to know or care about implementation and it can satisfy all of the requirements of my pre-existing API with only minor modification (in the end at least). I'm not sure if this is what the answer intended as a result but it is functional and provides the simplicity necessary.
Hopefully my headaches will help someone else in the future.

Is it applicable to place methods inside ASP.NET MVC views?

The current code of one of the views in my project is a large, monolithic one and I just want to organize it. Is it just fine to place methods inside my ASP.NET MVC view?
I would say definitely not. The purpose of MVC is to separate the concerns i.e. Model - View - Controller. In your case you are mixing Model/Controller logic with your View.
If you need to pass complex content into your view you should create custom ViewData classes, populate them in your controller and pass them into your View.
If your methods are more relating to generating View markup you should look at splitting it up into partial views or as already suggested using helper extension methods.
It's possible but if you need methods inside a view, perhaps you should instead consider extending the Html object with an extension method, and then use these methods from inside the view.
I like to separate my extensions by functionality to keep the view readable.
For example:
public static MySpecialDateHelper SpecialDateHelper(this HtmlHelper helper)
{
return new MySpecialDateHelper(helper);
}
public class MySpecialDateHelper
{
// Fields
private HtmlHelper m_helper;
private StringBuilder m_sb = new StringBuilder();
// Methods
internal MySpecialDateHelper(HtmlHelper helper)
{
this.m_helper = helper;
}
// Print date prettily
public public string PrettyDate(datetime target)
{
// format however
return string.format("pretty {0}", target.ToString());
}
// Print date prettily
public public string PrettyDateInATextbox(datetime target)
{
return m_helper.TextBox("prettyid",PrettyDate(target));
}
// etc...
}
Then in your view, you just have to reference the extension you just created
<%= Html.SpecialDateHelper.PrettyDateInATextbox(Now); %>
Of course, your separation of methods into extensions may vary, you can also extend the HtmlHelper object with the methods directly, but i prefer this method
No.
Move it the controller, if necessary introduce view models. After the that, define your own Html helpers, considering those should be for html like stuff, not for any logic.
You could have something in a view that's v. v. specific to it, and its just not possible that bit of code would do anything outside of it. But even then, you can just define it in a helper, as long as the name is very clear on what it does, you are better than putting it in the view. You can unit test helpers, so there is a clear advantage when doing that.

Categories