I'm playing with MVC3 using the Razer syntax, though I believe the problem to be more general.
In the controller, I have something like:
ViewModel.User = New User(); // The model I want to display/edit
ViewModel.SomeOtherProperty = someOtherValue; // Hense why need dynamic
Return View();
My View inherits from System.Web.Mvc.ViewPage
But if I try to do something like:
<p>
#Html.LabelFor(x => x.User.Name
#Html.EditorFor(x => x.User.Name
</p>
I get the error: "An expression tree may not contain a dynamic operation"
However, the use of ViewPage seems quite common, as are EditorFor/LabelFor. Therefore I'd be surprised if there's not a way to do this - appreciate any pointers.
Don't use ViewPage<Dynamic>. I would recommend you using a view model and strongly type your view to this view model:
var model = new MyViewModel
{
User = new User
{
Name = "foo"
},
SomeOtherProperty = "bar"
};
return View(model);
and then strongly type your view to ViewPage<MyViewModel> and:
#Html.LabelFor(x => x.User.Name)
#Html.EditorFor(x => x.User.Name)
<div>#Model.SomeOtherProperty</div>
It seems the expression trees => http://msdn.microsoft.com/en-us/library/bb397951.aspx must not contain any dynamic variables.
Unfortunately this is the case for TModel when you use dynamics in it.
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func> expression
)
Related
Call me crazy but this seems incredibly redundant and unnecessary but for the life of me I can't find a function that achieves both.
using Html.BeginForm("Save, Home") {
#Html.DisplayFor(x => x.Id)
#Html.HiddenFor(x => x.Id)
#Html.DisplayFor(x => x.CreatedDate)
#Html.HiddenFor(x => x.CreatedDate)
//Useful fields below
}
Let me explain - I want to save the CreatedDate and Id values, hence the hiddenFor. This of course generates an input behind the scenes, but I also need to display it. DisplayFor, and ValueFor both return Id = 0 on my form submission. More specifically when I update the data in the form, I would like to retain those two values. The model returned has them at 0/Datetime year 0.
Is there not a way to save those values for the next model?
You could create an extension method:
public static class HtmlHelperExtensions
{
public static IHtmlString DisplayAndHiddenFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Func<TModel, TProperty> propertyAccessor)
{
return new HtmlString(
helper.DisplayFor(propertyAccessor).ToHtmlString()
+ helper.HiddenFor(propertyAccessor).ToHtmlString());
}
}
Used like this (with the proper using of course):
#Html.DisplayAndHiddenFor(x => x.Id)
#Html.DisplayAndHiddenFor(x => x.CreatedDate)
I've been trying out the PagedList package to get paging for my index views. Everything was going well, and at the controller level everything is working fine, it only displays 5 records per page, and displays the appropriate page based on the querystring.
My problem is in the view. I changed the #Model to PagedList.IPagedList so I could access the Model.HasNextPage and other properties, but now the #Html.DisplayNameFor(model => model.ItemName) are no longer working. I get this error:
PagedList.IPagedList<Dossier.Models.Item>' does not contain a definition for 'ItemName' and no extension method 'ItemName' accepting a first argument of type 'PagedList.IPagedList<Dossier.Models.Item>' could be found (are you missing a using directive or an assembly reference?)
Here are the relevant parts of the view:
#model PagedList.IPagedList<Dossier.Models.Item>
#using Dossier.Models.Item
...
<th>
#Html.DisplayNameFor(model => model.ItemName)
</th>
It seems IPagedList is not compatible with DisplayNameFor(). Any idea why this is happening, and how I could fix it? I know I could just manually enter the column names, but I'd like for that information to stay (and be changeable) in the model later.
You can try this
#Html.DisplayNameFor(model => model.FirstOrDefault().ItemName)
As an alternate solution to the accepted answer, remember that IPagedList inherits from IEnumerable. That means that you could write:
#model IEnumerable<Dossier.Models.Item>
At the beginning of the page, and just cast the model to IPagedList when needed:
#Html.PagedListPager((IPagedList)Model, page => Url.Action("Index", new { page = page }))
You can even declare the casted variable in the header, in order to use it multiple times within the page:
#{
ViewBag.Title = "My page title";
var pagedlist = (IPagedList)Model;
}
This would allow you to use the DisplayNameFor helper method, and access all PagedList methods/properties, without the need for dummy elements nor calling .FirstOrDefault() for each field.
I solved the problem by creating an overload of DisplayNameFor that accepts a IPagedList<TModel>.
namespace PagedList.Mvc
{
public static class Extensions
{
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<IPagedList<TModel>> html, Expression<Func<TModel, TValue>> expression)
{
return DisplayNameForInternal(html, expression);
}
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "This is an extension method")]
internal static MvcHtmlString DisplayNameForInternal<TModel, TValue>(this HtmlHelper<IPagedList<TModel>> html, Expression<Func<TModel, TValue>> expression)
{
return DisplayNameHelper(ModelMetadata.FromLambdaExpression(expression, new ViewDataDictionary<TModel>()),
ExpressionHelper.GetExpressionText(expression));
}
internal static MvcHtmlString DisplayNameHelper(ModelMetadata metadata, string htmlFieldName)
{
string resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last();
return new MvcHtmlString(HttpUtility.HtmlEncode(resolvedDisplayName));
}
}
}
I'll be sending a pull request to PageList project to include it into the project for everyone.
You do not need to change #Html.DisplayNameFor. Declare model in the view as:
#model IEnumerable<Dossier.Models.Item>
Just move your pager to partial view (lets name it "_Pager"):
#model IPagedList
...
#Html.PagedListPager(Model,
page => Url.Action("Index", new { page, pageSize = Model.PageSize }))
...
Render the pager in your view:
#Html.Partial("_Pager", Model)
Thats it.
P.S. You can create Html helper instead of partial view...
As an alternate solution you could try:
#Html.DisplayNameFor(x => x.GetEnumerator().Current.ItemName)
It will work even if the list is empty!
I have created several MVC templates for the EditorFor and DisplayFor helper methods to style things the way I wanted using the Twitter Bootstrap framework. I now have a working solution for all the bits I need, but would like to generalize one part I set up to show a list of states. I have a State enum (with a list of all US states) that I display in a drop down for a users address. I used the [DataType] attribute to get MVC to use my State.cshtml template.
[Required]
[Display(Name = "State")]
[DataType("State")]
public State State { get; set; }
So it works nicely, but I would like to change it so that I can do something like DataType("Enum") or some other way to hit this template generically for all enums.
The template looks like this:
#using System
#using System.Linq
#using Beno.Web.Helpers
#using TC.Util
#model Beno.Model.Enums.State
<div class="control-group">
#Html.LabelFor(m => m, new {#class = "control-label{0}".ApplyFormat(ViewData.ModelMetadata.IsRequired ? " required" : "")})
<div class="controls">
<div class="input-append">
#Html.EnumDropDownListFor(m => m)
<span class="add-on">#(new MvcHtmlString("{0}".ApplyFormat(ViewData.ModelMetadata.IsRequired ? " <i class=\"icon-star\"></i>" : "")))</span>
</div>
#Html.ValidationMessageFor(m => m, null, new {#class = "help-inline"})
</div>
</div>
The EnumDropDownListFor is a helper method I posted about before and that works generically with any enum. What I don't know is how would I change this template to take a generic enum as the model object?
UPDATE: For completeness I include a listing of the EnumDropDownListFor method:
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes = null) where TProperty : struct, IConvertible
{
if (!typeof(TProperty).IsEnum)
throw new ArgumentException("TProperty must be an enumerated type");
var selectedValue = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model.ToString();
var selectList = new SelectList(from value in EnumHelper.GetValues<TProperty>()
select new SelectListItem
{
Text = value.ToDescriptionString(),
Value = value.ToString()
}, "Value", "Text", selectedValue);
return htmlHelper.DropDownListFor(expression, selectList, htmlAttributes);
}
Changing the model type to Enum produces the following error on the line with the call to the helper method:
CS0453: The type 'System.Enum' must be a non-nullable value type in order to use it as parameter 'TProperty' in the generic type or method 'Beno.Web.Helpers.ControlHelper.EnumDropDownListFor<TModel,TProperty>(System.Web.Mvc.HtmlHelper<TModel>, System.Linq.Expressions.Expression<System.Func<TModel,TProperty>>, object)'
Then if I remove the check if TProperty is an enum and the struct where constraint, I get a compile error on the line where I am trying to get the enum values of:
System.ArgumentException: Type 'Enum' is not an enum
I wonder if it's just not possible to do what I am trying here.
You could just create an EditorTemplate Enum.cshtml
All you would have to do is change this line :
#model Beno.Model.Enums.State
For this :
#model System.Enum
You will then be able to use any Enum with it.
The catch: the engine can't infer the base class of an item thus, TestEnum won't be assigned the Enum template, so you would have to call it explicitly :
#Html.EditorFor(model => model.EnumValue, "Enum")
Not sure if I understand exactly what you mean, but try this:
#Html.DropDownListFor(model => model.EnumName, new SelectList(Enum.GetValues(typeof(Namespace.Models.EnumName))))
EnumName = State in your case.
I've used the above to get an enum into a drop down list using Twitter Bootstrap.
I too have been trying to achieve this.
Is the idea that you want to be able to use one template for all Enum types in all your models.
This way you have an Enum Template in the EditorTemplates folder that allow you to display them as drop down lists.
I have been following this article. http://blogs.msdn.com/b/stuartleeks/archive/2010/05/21/asp-net-mvc-creating-a-dropdownlist-helper-for-enums.aspx
The issue you have is that your template passes the type of System.Enum in the TModel and TProperty
Expression<Func<TModel, TProperty>> expression
Then when you perform the following below TProperty is of Type System.Enum not Beno.Model.Enums.State
EnumHelper.GetValues<TProperty>()
To get around this I do not bother looking at TProperty as it does not give me the right type.
Instead I look at the metadata.ModelType.
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
This gives me the correct type but you can't use these in within the Covariance Derived class
EnumHelper.GetValue<metadata.ModelType> //This does not work.
So I rewrote the body to not use any generics.
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var values = Enum.GetValues(metadata.ModelType);
List<SelectListItem> items = new List<SelectListItem>();
foreach (var v in values)
{
items.Add(new SelectListItem
{
Text = Regex.Replace(v.ToString(), "([A-Z][a-z])", " $1").Trim(),
Value = v.ToString(),
Selected = v.Equals(metadata.Model)
});
}
return htmlHelper.DropDownListFor(expression, items);
}
You may need to change the method signature to include your htmlattributes.
As others show, writing a custom helper is the way to go. This is exactly what was done in TwitterBootstrapMVC. Among other helpers it has a helper DropDownListFromEnumFor(...), which you'd use like so:
#Html.Bootstrap().DropDownListFromEnumFor(m => m.SomeEnum)
or
#Html.Bootstrap().DropDownListFromEnum("SomeEnum")
The cool thing about BMVC is that you can customize the dropdown with extension methods some of which are for regular html and others are Bootstrap specific. Below are some of them:
#(f.ControlGroup().DropDownListFromEnumFor(m => m.SomeEnum)
.Append("something")
.AppendIcon("glyphicon glyphicon-chevron-right")
.Class("cool-dd")
.OptionLabel("-- Select --")
.Tooltip("cool tooltip"))
Oh, and yeah, the example above will generate full control-group - input, label, and validation message.
Disclaimer: I'm the author of TwitterBootstrapMVC
let me ask the question first.
Where is the correct place to call a function that load a list of values to be display on a view?
I create a controller like this
public ActionResult Create()
{
SeaModel newSea = new SeaModel();
return View("Season/CreateSea", newSea);
}
//I not quite sure if this should go here or in another place
partial class seaDataContext
{
public List<string> getSeaSettings()
{
var seaSettings = from p in settings
where p.setting == "periods"
select p.value;
return seaSettings.ToList<string>();
}
}
The model is like
public class SeaModel
{
[Required(ErrorMessage="*")]
[Display(Name = "Period Name")]
public string periods { get; set; }
}
Which create a view like
#using (Html.BeginForm()) {
#Html.ValidationSummary(true, "Please correct the following errors.")
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
#Html.LabelFor(model => model.periods)
</div>
<div class="editor-field">
#Html.Select(model => model.periods, ****My doubt comes here****)
#Html.ValidationMessageFor(model => model.periods)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
so, How and where do I pass the return of getSeaSettings() to the view?
Thanks
best practice is to make a Selectlist in your Model for this dropdown.
however you also can use the more easy option: using ViewData
public ActionResult Create()
{
SeaModel newSea = new SeaModel();
ViewData["myDropDown"] = new SelectList(listOfObjects, "valueOfTheObjectLikeID", "NameYouWantToShowInDropdown");
return View("Season/CreateSea", newSea);
}
then:
#Html.Select(model => model.periods, ViewData["myDropDown"] as SelectList)
dont forget in your [HttpPost] method to also fill in the viewdata if you'r validation fails, so the dropdown can be rebuilt.
You need to look at repository pattern. Have a look at this tutorial at asp.net site
http://www.asp.net/mvc/tutorials/creating-model-classes-with-linq-to-sql-cs
Stefanvds's approach was what I used to do.
But I found out there is a better way using additionalViewData.
Use this EditorFor HTML Helper extension method.
http://msdn.microsoft.com/en-us/library/ff406462.aspx
Instead of passing Select List Items into ViewData in the Controller, you do this in your View.
Pass in your list items as an anonymous object for the additionalViewData parameter.
Important thing is to use the same name as your Property Name.
#Html.EditorFor(
m => m.MyPropertyName,
new { MyPropertyName = Model.ListItemsForMyPropertyName }
);
Of course, you are passing in a View Model object.
public class MyViewModel
{
public int MyPropertyName;
public IList<SelectListItem> ListItemsForMyPropertyName;
}
EditorFor method uses your existing Editor View Templates.
So you don't need to specify CSS class names and HTML attributes again like when you use the Html.DropDown( ) method.
For example,
//------------------------------
// additionalViewData
//------------------------------
#Html.EditorFor(
m => m.MyPropertyName,
new { MyPropertyName = Model.ListItemsForMyPropertyName }
)
//------------------------------
// traditional approach requires to pass your own HTML attributes
//------------------------------
#Html.DropDown(
"MyPropertyName",
Model.ListItemsForMyPropertyName,
new Dictionary<string, object> {
{ "class", "myDropDownCssClass" }
}
);
//------------------------------
// DropDownListFor still requires you to pass in your own HTML attributes
//------------------------------
#Html.DropDownListFor(
m => m.MyPropertyName,
Model.ListItemsForMyPropertyName,
new Dictionary<string, object> {
{ "class", "myDropDownCssClass" }
}
);
That is why I like the additionalViewData approach more.
Because, the HTML code rendered relies on the Editor Templates completely.
Also, using specialized View Models make your code cleaner and easier to maintain.
Hope it helps.
Once again I'm confronted with a "This shouldn't be this ?*!# hard" situation.
Problem: I want to use a form in MVC for creation of an object. One of the elements of the object is a set of limited choices - a perfect candidate for a drop down list.
But if I use a SelectList in my model, and a drop down list in my View, and then try to post the Model back to my Create method, I get the error "Missing Method Exception:No Parameterless constructor for this object". Exploring the MVC source code, it appears that in order to bind to a model, the Binder has to be able to create it first, and it can't create a SelectList because there is no default constructor for it.
Here's the simplified code:
For the model:
public class DemoCreateViewModel
{
public SelectList Choice { get; set; }
}
For the controller:
//
// GET: /Demo/Create
public ActionResult Create()
{
DemoCreateViewModel data = new DemoCreateViewModel();
data.Choice = new SelectList(new string[] { "Choice1", "Choice2", "Choice3" });
ViewData.Model = data;
return View();
}
//
// POST: /Demo/Create
[HttpPost]
public ActionResult Create(DemoCreateViewModel form)
{
try
{
// TODO: Add insert logic here
return RedirectToAction("Index");
}
catch
{
return View();
}
}
And for the View:
<fieldset>
<legend>Fields</legend>
<%= Html.LabelFor(model => model.Choice) %>
<%= Html.DropDownListFor(model => model.Choice, Model.Choice) %>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
Now, I know I can MAKE this work by dropping back 10 yards and punting: bypass model binding and drop back to the FormCollection and validate and bind all the fields myself, but there's got to be a simpler way. I mean, this is about as simple a requirement as it gets. Is there a way to make this work within the MVC ModelBinding architecture? If so, what is it? And if not, how come?
Edit: Well, I have egg on my face, but maybe this will help someone else. I did some more experimenting and found a simple solution that seems to work.
Provide a simple value (string or integer, depending on what your select list value type is), and name that as the model element that you bind to. Then provide a second element as the select list of choices, and name it something else. So my model became:
public class DemoCreateViewModel
{
public string Choice { get; set; }
public SelectList Choices { get; set; }
}
And then the DropDownListFor statement in the View becomes:
<%= Html.DropDownListFor(model => model.Choice, Model.Choices) %>
When I do this, the submit button correctly binds the choice made in the form to the string Choice, and submits the model back to the second Create method.
Here is one approach:
#Html.DropDownListFor(model => model.Choice,
ViewBag.Choices as SelectList,
"-- Select an option--",
new { #class = "editor-textbox" })
Notice that I use ViewBag to contain my SelectList. This way when you post back, the client doesn't send the entire select list up to the server as part of the model.
In your controller code, you just need to set the view bag:
ViewBag.Choices = new SelectList(....
Consider creating a different view model for your post action without the SelectList property:
public class DemoCreateViewModelForUpdate
{
public string Choice { get; set; }
}
Then you can always map from the DemoCreateViewModelPost instance to an DemoCreateViewModel instance if the model state is invalid and you want to re-show the view. I tend to prefer everything needed by the view to be in my display view model class, so using a separate update only view model let's me keep things slim and trim for the trip back to the server.
In your view, you'd do:
#Html.DropDownListFor(m => m.Choice, Model.Choices)
as in the previous answer, so no unnecessary data would round trip.