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!
Related
My view page like this;
#model IEnumerable<Project.ViewModels.ViewModel>
#using Helpers
#foreach (var item in Model)
{
#Html.MyCustomHtmlHelper("test")
}
My custom HtmlHelper like this;
public static MvcHtmlString MyCustomHtmlHelper(this HtmlHelper helper, string TestValue)
{
var builder = new StringBuilder();
builder.Append(TestValue);
return MvcHtmlString.Create(builder.ToString());
}
It works with #model Project.ViewModels.ViewModel but not #model IEnumerable<Project.ViewModels.ViewModel>
My error;
The code you have shown in your question:
#foreach (var item in Model)
{
#Html.MyCustomHtmlHelper("test")
}
doesn't correspond to the code shown in the YSOD:
<td>
#Html.MyCustomHtmlHelper(TestValue)
</td>
It looks like in your real code (which you haven't shown us), you have used some TestValue variable. Unfortunately it's pretty hard to know how/where/of what type is this variable declared but if it is not a string it is more than obvious that your custom helper won't work for the simple reason that this helper expects a string parameter.
Case:
I have a list of items of Class X displayed using Editor Template for Class X.
Problem:
How can I get index of an item being processed on the inside of the Editor Template?
I've been using this HtmlExtension that returns only the needed id of an iteration. It's basically a regex on ViewData.TemplateInfo.HtmlFieldPrefix that's capturing the last number.
public static class HtmlExtensions
public static MvcHtmlString Index(this HtmlHelper html)
{
var prefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
var m = Regex.Match(prefix, #".+\[(\d+)\]");
if (m.Success && m.Groups.Count == 2)
return MvcHtmlString.Create(m.Groups[1].Value);
return null;
}
}
Can be used in an EditorFor-template like this:
#Html.Index()
Use a for loop instead of for each and pass the indexer into the EditorFor extension; razor should handle the rest.
#for(var i = 0; i < Model.count(); i++)
{
#Html.EditorFor(m => Model.ToArray()[i], new { index = i })
}
Update:
pass in the the index of the item using view data as show above.
In your editor template access the item via the ViewBag
<span> Item Index: #ViewBag.index </span>
Using the EditorTemplate is the best solution when viewing models that contain a list of something.
In order to find the index for the sub-model being rendered you can use the property that Razor sets by default:
ViewData.TemplateInfo.HtmlFieldPrefix
Say, for example, you have the following view models:
public class ParagraphVM
{
public int ParagraphId { get; set; }
public List<LineVM> Lines { get; set; }
}
and
public class LineVM
{
public int Id { get; set; }
public string Text {get; set;}
}
and you want to be able to edit all the "LineVM" within a "ParagraphVM". Then you would use an Editor Template so you would create a view at the following folder (if it doesn't exist) with the same name as the sub-model Views/Shared/EditorTemplates/LineVM.cshtml:
#model MyProject.Web.MVC.ViewModels.Paragraphs.LineVM
#{
//this will give you the List's element like Lines[index_number]
var field = ViewData.TemplateInfo.HtmlFieldPrefix;
}
<div id="#field">
#Html.EditorFor(l => l.Text)
</div>
Assuming you have a Controller's ActionResult that is returning a View and passing a ParagrapghVM viewmodel to a view, for example Views/Paragraph/_Paragraph.cshtml:
#model MyProject.Web.MVC.ViewModels.Paragraphs.ParagraphVM
#using (Html.BeginForm("Details", "Paragraphs", FormMethod.Post))
{
#Html.EditorFor(p => p.Lines)
}
This view would render as many editors for the list Lines as items contains that list.
So if, for example, the property list ParagraphVM.Lines contains 3 items it would render something like:
<div id="#Lines[0]">
<input id="Lines_0__Text name="Lines[0].Text"/>
</div>
<div id="#Lines[1]">
<input id="Lines_1__Text name="Lines[1].Text"/>
</div>
<div id="#Lines[2]">
<input id="Lines_2__Text name="Lines[2].Text"/>
</div>
With that you can know exactly what position each items is within the list and for example use some javascript to create a carousel or whatever you want to do with it. But remember that to edit that list you don't really need to know the position as Razor takes care of it for you. If you post back the model ParagraphVM, the list Lines will have the values bound (if any) without any additional work.
How about:
#using System
#using System.Text.RegularExpressions
var i = Convert.ToInt32(Regex.Matches(
ViewData.TemplateInfo.HtmlFieldPrefix,
#"\[([0-9]+)?\]")[0].Groups[1].ToString());
I think the easiest way is:
#Regex.Match(ViewData.TemplateInfo.HtmlFieldPrefix, #"(?!\[)\d+(?=\])")
Or as helper:
public static string Index(this HtmlHelper html)
{
Match m = Regex.Match(html.ViewData.TemplateInfo.HtmlFieldPrefix, #"(?!\[)\d+(?=\])");
return m.Success ? m.Value : null;
}
Inspired by #Jona and #Ryan Penfold
You can use #Html.NameFor(m => m.AnyField). That expression will output the full name property including the index. You could extract the index there...
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
I'm currently trying to post a form composed of two strongly typed views. This question is similar but it doesn't have an answer:
MVC 3 Razor Form Post w/ Multiple Strongly Typed Partial Views Not Binding
When I submit form the model submitted to the controller is always null. I've spent a couple of hours trying to get this to work. This seems like it should be simple. Am I missing something here? I don't need to do ajax just need to be able to post to the controller and render a new page.
Thanks
Here's my view code:
<div>
#using (Html.BeginForm("TransactionReport", "Reports", FormMethod.Post, new {id="report_request"}))
{
ViewContext.FormContext.ValidationSummaryId = "valSumId";
#Html.ValidationSummary(false, "Please fix these error(s) and try again.", new Dictionary<string, object> { { "id", "valSumId" } });
#Html.Partial("_ReportOptions", Model.ReportOptions);
#Html.Partial("_TransactionSearchFields", new ViewDataDictionary(viewData) { Model = Model.SearchCriteria });
}
Here's the code in the controller:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult TransactionReport(TransactionReportRequest reportRequest)
{
var reportInfo = new List<TransactionReportItem>();
if (ModelState.IsValid)
{
var reportData = _reportDataService.GetReportData(Search.MapToDomainSearchCriteria(reportRequest.SearchCriteria));
if (reportData!=null)
{
reportInfo = reportData.ToList();
}
return View(reportInfo);
}
return View(reportInfo);
}
The partial views themselves are pretty irrelevant since all they are doing is biding and displaying their models.
Partials are not the way to go here. You are looking for EditorTemplates, these are made for what you want. This case, your properties will be nicely bound to your model (that you will submit).
Your main View will have this form (note that you only have to use EditorFor instead of Partial; in this case, you probably will need to put that viewData parameter in the ViewBag or so):
#using (Html.BeginForm("TransactionReport", "Reports", FormMethod.Post, new {id="report_request"}))
{
ViewContext.FormContext.ValidationSummaryId = "valSumId";
#Html.ValidationSummary(false, "Please fix these error(s) and try again.", new Dictionary<string, object> { { "id", "valSumId" } });
#Html.EditorFor(model => model.ReportOptions);
#Html.EditorFor(model = Model.SearchCriteria });
}
Now you only have to drag your partials to the folder ~/Shared/EditorTemplates/ and rename them to match the model name they are the editor templates for.
In the ~/Shared/EditorTemplates/ folder, make a new "view", example "SearchCriteria.cshtml". Inside, put as "model" the type of class you which to create an editor template for. Example (example class has properties Name and OtherCriteria):
#model MyNamespace.SearchCriteria
<ul>
<!-- Note that I also use EditorFor for the properties; this way you can "nest" editor templates or create custom editor templates for system types (like DateTime or String or ...). -->
<li>#Html.LabelFor(m => m.Name): #Html.EditorFor(m => m.Name)</li>
<li>#Html.LabelFor(m => OtherCriteria): #Html.EditorFor(m => m.OtherCriteria</li>
</ul>
Some good reading about them:
https://www.exceptionnotfound.net/asp-net-mvc-demystified-display-and-editor-templates/
https://www.hanselman.com/blog/ASPNETMVCDisplayTemplateAndEditorTemplatesForEntityFrameworkDbGeographySpatialTypes.aspx
You should add prefix to the PartialView's fields. That will let binding data correctly.
So instead:
#Html.Partial("_ReportOptions", Model.ReportOptions);
Use:
#Html.Partial("_ReportOptions", Model.ReportOptions, new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "ReportOptions" }})
I agree with #Styxxy and #Tony, Editor Templates are the better solution. However, your problem is that that you are feeding a sub-model to the partial views. Thus, when the partial view renders it doesn't know that it's part of a larger model and does not generate the correct name attributes.
If you insist on using Partials rather than Editor Templates, then I suggest only passing the Model to the partials, then having each partial do Model.Whatever.Foo and it will generate the correct name attributes for binding.
Try using EditorTemplates instead of Partials http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/.
#Html.Partial("_ReportOptions", Model.Contact, new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "Contact"
}
})
)
#Html.Partial("_TransactionSearchFields", Model.SearchCriteria, new
ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "SearchCriteria"
}
})
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
)