How to get sequence/array index in Editor Template? - asp.net-mvc

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...

Related

How to configure an MVC dropdown depending on which view calls it

I have two views, BatchReceipt and Receipt which utilise the same model. Until now they have used the same display template of ReceiptType. But I want to have one exclude certain items and the other to have the full list (so essentially a second .cshtml display template called ReceiptTypeFull). How do I configure each of these views in Visual Studio to utilise the different Display Templates?
Some additions to show the code being used:
I have file ReceiptType.cshtml being used as a DisplayTemplate which contains the following to setup the receipt dropdown
#using Clinton.Web.Helpers.EnumHelpers
#{
var item = EnumsHelper.GetNameFromEnumValue(Model);
}
I want to use a different DisplayTemplate, call it ReceiptTypeFull.cshtml
#using Clinton.Web.Helpers.EnumHelpersFull
#{
var item = EnumsHelper.GetNameFromEnumValue(Model);
}
#item
The difference is in calling the enumhelper or the enumhelperfull to vary the query populating the dropdown. My problem is that I cannot see how to redirect the view to use the different enumhelper/displaytemplate/
Thanks
I think I understand what you are getting at. You want to control which template is used for an Enum in the view.
I will explain using editor templates but it works the same way if you use display templates. You should be able to follow and apply for your scenario.
The idea is to use this overload of the editor html helper.
public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName);
It is called like this
#Html.Editor("{property name}", "{template name}").
Below is an example to show it being used.
Suppose we have this enum
public enum MyItems
{
Item1 = 1,
Item2 = 2,
Item3 = 3
}
This helper
public static class MyEnumHelper
{
public static List<MyItems> GetAllItems()
{
return new List<MyItems>()
{
MyItems.Item1,
MyItems.Item2,
MyItems.Item3
};
}
public static List<MyItems> GetSomeItems()
{
return new List<MyItems>()
{
MyItems.Item1,
MyItems.Item2
};
}
}
This controller
public class HomeController : Controller
{
public ActionResult AllItems()
{
return View();
}
public ActionResult SomeItems()
{
return View();
}
}
We have these 2 editor templates, which are put in views/shared/editortemplates
First one called MyItems.cshtml which is the all one
#model MyItems?
#{
var values = MyEnumHelper.GetAllItems().Cast<object>()
.Select(v => new SelectListItem
{
Selected = v.Equals(Model),
Text = v.ToString(),
Value = v.ToString()
});
}
#Html.DropDownList("", values)
Second one called MyItems2.cshtml which is the some one
#model MyItems?
#{
var values = MyEnumHelper.GetSomeItems().Cast<object>()
.Select(v => new SelectListItem
{
Selected = v.Equals(Model),
Text = v.ToString(),
Value = v.ToString()
});
}
#Html.DropDownList("", values)
Then in the AllItems.cshtml to get the MyItems.cshtml template called we need
#model MyItemsViewModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.MyItem)
<submit typeof="submit" value="submit"/>
}
And in the SomeItems.cshtml to get some of the items by calling MyItems2.cshtml we use
#model MyItemsViewModel
#using (Html.BeginForm())
{
#Html.Editor("MyItem", "MyItems2") #* this bit answers your question *#
<submit typeof="submit" value="submit" />
}

How to use passed select list in an editor template

I'm trying to create an editor template that will create a "bootstrap style" radio buttons for each value from a passed select list (just like the Html.DropDownFor method create a dropdown list)
So i have the call in my view:
#Html.EditorFor(model => model.FaultTypeID,"RadioButtonList",
new SelectList(Model.AllowdeFaultTypes, "FaultTypeID", "FaultTypeName"))
and now the template of RadioButtonList:
#foreach (var item in ViewData["Items"] as SelectList)
{
<a>#item.Text</a> <b>#item.Value</b>
}
but the conversion fails and i get a NullReferanceExeption.
By reflection i see that the ViewData["Items"] value is of type System.Collections.Generic.List<CamelotFaultManagement.DAL.FaultType>
The problem is i really don't want to tightly couple the RadioButtonList editor template with CamelotFaultManagement.DAL.FaultType class, its just don't make any sense to do that. I want a generic editor template.
In your editor template you seem to be using some ViewData["Items"] property which you never set. If you want to use such property make sure you have assigned it:
#Html.EditorFor(
model => model.FaultTypeID,
"RadioButtonList",
new { Items = new SelectList(Model.AllowdeFaultTypes, "FaultTypeID", "FaultTypeName") }
)
This being said, your approach with using some ViewData stuff seems totally wrong to me.
I would simply define a view model (as always in ASP.NET MVC):
public class RadioListViewModel
{
public string Value { get; set; }
public IEnumerable<SelectListItem> Values { get; set; }
}
and then you could have your editor template strongly typed to this view model. Of course your editor template will be now stored in ~/Views/Shared/EditorTemplates/RadioListViewModel.cshtml:
#model IRadioListViewModel
#foreach (var item in Model)
{
<a>#item.Text</a> <b>#item.Value</b>
}
and now all that's left is to use this view model in your main view model:
public class MyViewModel
{
public RadioListViewModel FaultTypes { get; set; }
...
}
and then inside your view simply render the corresponding editor template:
#model MyViewModel
...
#Html.EditorFor(x => x.FaultTypes)
Simple, conventional, strongly typed.

how to find generic list values by index

I have a Generic List that it contains 4 value .
how can I get my generic list values by index ? I want to get all values in generic list
this is my code :
var Checked = (form.GetValues("assignChkBx")).ToList();
string str = "";
for (int i = 0; i < Checked.Count; i++)
{
str = str + Checked[i]. +",";
}
in this code I got all checkboxes values that checked . Now I want to get all values . how can I get values ?
Your question about the generic list is very misleading and I suspect that it has nothing to do with your real problem.
Depending on how you generated the checkboxes inside your view that might be possible or not. If you hardcoded them using directly an <input type="checkbox"> tag values of checkboxes that were not checked will never be sent to the server - that's how HTML checkboxes work. In this case you will not be able to get all values. If on the other hand you used the Html.CheckBoxFor helper to generate them then you will notice that this helper adds a hidden field to each checkbox in order to send all values. This helper operates on boolean values though.
So I would recommend you creating a view model that will contain 2 properties: one holding the values you are interested in and one boolean property indicating whether the user selected this value or not in the view:
public class ItemViewModel
{
public string Value { get; set; }
public bool Checked { get; set; }
}
and then have a view model which has collection of those items:
public class MyViewModel
{
public ItemViewModel[] Items { get; set; }
}
Now inside your view you can render those values like this:
#model MyViewModel
#using (Html.BeginForm())
{
for ( var i = 0; i < Model.Items.Length; i++)
{
<div>
#Html.CheckBoxFor(x => x.Items[i].Checked)
#Html.HiddenFor(x => x.Items[i].Value)
</div>
}
<button type="submit">OK</button>
}
and finally inside the controller action that this form will be submitted to you will be able to get all values:
[HttpPost]
public ActionResult SomeAction(MyViewModel model)
{
foreach (var item in model.Items)
{
// here you could use item.Checked and item.Value
}
...
}

Passing only one object of a ViewModel to controller

I have a Home ViewModel class which contains others class:
public class HomeVM
{
public ProductSearchRequest ProductSearchRequest { get; set; }
//Other class
//Other class
//Other class
}
My home/Index view has #model HomeVM, and there I have a search text input:
#using (Html.BeginForm("Search", "Product"))
{
#Html.TextBoxFor(m => m.ProductSearchRequest.SearchText)
<input type="submit" value="Search" />
}
But in my Product/Search I need to receive only ProductSearchRequest because there is other pages that uses other ViewModel but contains ProductSearchRequest.
I'm trying this:
public ActionResult Search(ProductSearchRequest request)
{
var response = new ProductSearchResponse
{
SearchText = request.SearchText,
Products = GetProductsByName(request.SearchText)
};
return View(response);
}
but it doesn't work.. request.SearchText is always null..
How can I do this?
This is just a stab in the dark, but your action, try calling the parameter 'ProductSearchRequest'
Or how about putting your whole form in a view for that action I.e. 'Search' that takes the 'ProductSearchRequest' as model?
The problem you are having is because TextBoxFor() will create an html input using a naming convention that is expected to be bound to the same type of model HomeVM as the original action.
You can try using the simple TextBox() helper method like this:
#Html.TextBox("SearchText", Model.ProductSearchRequest.SearchText)
To avoid using a string value, you can make a separate form partial that takes the ProductSearchRequest object as the model, and call:
#Html.RenderPartial("SearchForm",Model.ProductSearchRequest)
Now you can use the TextBoxFor() method like this in the partial:
#Html.TextBoxFor(m => m.SearchRequest)

DRY in the MVC View

I've been working a lot with asp.net web forms and one think that I like about the is the consistency with the generated markup e.g. if you create a composite control for a TextField you can control the generated markup in a single class like and don't break the SRP:
<form:textfield id="firstName" runat="server" required="true" label="First Name" />
I you're your going to generate the markup by hand it might look like this:
<label for="firstName" id="lbl_firstName">Name <span class="required">*</span></label>
<input id="firstName" name="firstName" type="text" value="" />
The problem is when would like to change something for example add a wrapping div or move the span. In worst case you have to edit thousands of views.
That's why I really like the MVC Contrib FluentHtml.
<%= this.TextBox(x => x.Message.PostedBy).Class("required").Label("Name") %>
My question is what do you think is the best way to add a wrapping div for the code line above? I think hand writing is not an option because of the arguments above? Perhaps extending the TextBox : MvcContrib.FluentHtml.Elements.TextInput?
have you checked InputBuilder in MvcContrib project? it is used in Codecampserver as well. have a look and i think u will like it.
Honestly, I don't think the example case you've given applies to real world. A textbox is a textbox. If you need one, you render one.
If you need a more "complex" control like a textbox wrapped in a div tag, then you can have a partial view for that.
For example, Model :
public class CustomControlModel {
public string Name { get; set; }
public string Value { get; set; }
public string Class { get; set; }
public bool WrapInDivTag { get; set; }
//you get the idea
}
Custom Control :
<%# Control Inherits="System.Web.Mvc.ViewUserControl<CustomControlModel>" %>
<%if (Model.WrapInDivTag) {%> <div> <% } %>
<%=Html.TextBox(Model.Name, Model.Value, new { #class = Model.Class })%>
<%if (Model.WrapInDivTag) {%> </div> <% } %>
And when rendering :
<%Html.RenderPartial("CustomControl",
new CustomControlModel { Name = "name", WrapInDivTag = true }); %>
That's a very simple example but I hope it explains why I suggested partial views. Don't forget that you can expose another property to get which tag to render etc.
InputBuilders are one option. With FluentHtml you could create a custom element, something like this:
public class TextBoxInContainer : TextInput<TextBox>
{
public TextBoxInContainer (string name) : base(HtmlInputType.Text, name) { }
public TextBoxInContainer (string name, MemberExpression forMember, IEnumerable<IBehaviorMarker> behaviors) : base(HtmlInputType.Text, name, forMember, behaviors) { }
protected override ToString()
{
divBuilder = new TagBuilder(HtmlTag.Div);
divBuilder.InnerHtml = ToString();
return divBuilder.ToString(TagRenderMode.SelfClosing);
}
}
To use this from your view you would extend IViewModelContainer something like this:
public static MyTextBox TextBoxInContainer <T>(this IViewModelContainer<T> view, Expression<Func<T, object>> expression) where T : class
{
return new TextBoxInContainer (expression.GetNameFor(view), expression.GetMemberExpression(), view.Behaviors)
.Value(expression.GetValueFrom(view.ViewModel));
}
Then if you want to change your container to a span sitewide, you change the ToString method of TextBoxInContainer.

Resources