I have the following class that is supposed to represent a hierarchical data structure
class Person
{
int Id { get; set; }
Person Parent { get; set; }
List<Person> Children { get; set; }
}
In my UI I receive a collection of Person where each one may have children.
I need to print out an XML with the following structure:
<root>
<Person id="1" parent_id="0" name="">
<Person id="5" parent_id="1" name="">
<Person id="10" parent_id="5" name="">
</Person>
</Person>
<Person id="6" parent_id="1" name="">
</Person>
</Person>
<Person id="2" parent_id="0" name="">
</Person>
</root>
Right now I wrote this but my code is not recursive.
Can you help me on getting this done using LINQ?
public XDocument GetHtmlWorkbookTree(List<Person> persons)
{
var document = new XDocument();
var root = new XElement("Person",
persons.Select(
r => new XElement("Person",
new XAttribute("id", r.Id))));
document.Add(root);
return document;
}
It's not that big a deal, it's the same as any recursion. Just pass on the XElement to the recursive function.
I'm thinking something like this:
public XDocument GetHtmlWorkbookTree(List<Person> persons)
{
var document = new XDocument();
Person rootperson = persons.SingleOrDefault( ... select the person that you want to start with ... );
var root = new XElement("Person", ... fill with attributes as you like ...);
document.Add(root);
foreach(var item in rootperson.Children)
{
AddChildRecursive(root, rootperson);
}
return document;
}
public void AddChildRecursive(XElement element, Person person)
{
foreach(var item in person.Children)
{
XElement newchild = new Element("Person", ... fill with attributes as you like ...);
element.Add(newchild);
AddChildRecursive(newchild, item);
}
}
I'm not sure that this will work as it is, since I don't have VS right now to test it, but I hope you get the idea!
Related
So I have a model, other properties removed for brevity:
public class OutOfLimitReasonViewModel
{
[Required]
public int ReasonId { get; set; }
[Required(ErrorMessage = "Other Reason is required")]
public string OtherReason { get; set; }
}
Of course I have an EditForm on the .razor page and the InputText I care about looks like this:
<InputText #bind-Value="model.OtherReason" class="form-control" disabled="#(!OtherReasonRequired)" />
<ValidationMessage For="#(() => model.OtherReason)" />
There is also a select that has the list of available Reason objects, one of which is Other.
I do have a property called OtherReasonRequired which graphically does what I want (either Enable or Disable the input based on if the selected Reason == "Other") so that code works:
public bool OtherReasonRequired
{
get
{
var result = false;
if (model.ReasonId > 0)
{
var reason = Reasons.Find(x => x.Id == model.ReasonId);
result = reason.Reason == "Other";
}
return result;
}
}
This works perfectly if I select Other and give OtherReason a value, the Save/Submit button is valid and it works.
My issue is when I have NOT selected Other. Graphically, the InputField does get disabled and grayed out. But the Save/Submit believes the model is invalid because there is no value in the OtherReason field.
Is there something I can do to get this to work?
Would love to have a dynamic attribute called RequiredIf.
But logically to me, I see this as a bug. If a control is disabled, it's value is irrelevant in my mind.
Please check the following. I don't use any of the built-in Data Annotation, but it's still very easy to check the input and let the user know what I want them to do:
<EditForm Model="#DisplayModel" OnValidSubmit="HandleValidSubmit" >
<InputRadioGroup TValue=int? #bind-Value=DisplayModel.ReasonID>
#for (int i=1; i< Reasons.Count(); i++){
<InputRadio Value=Reasons[i].ID /> #Reasons[i].Description <br/>
}
<InputRadio Value=0 />Other <br/>
</InputRadioGroup>
<input #bind=DisplayModel.OtherText disabled=#((DisplayModel.ReasonID??1) !=0 ) />
<button type="submit">Submit</button>
<div class="text-danger">#DisplayMessage</div>
</EditForm>
#code {
string DisplayMessage="";
class Reason { public int? ID; public string? Description; }
List<Reason> Reasons = new List<Reason> {
new Reason { ID = 0, Description = "Other"},
new Reason { ID = 1, Description = "You're lame" },
new Reason { ID = 2, Description = "I'm too busy" }
};
class MyDisplayModel
{
public int? ReasonID;
public string OtherText="";
}
MyDisplayModel DisplayModel = new MyDisplayModel();
private async Task HandleValidSubmit()
{
if(DisplayModel.ReasonID is not null) if (DisplayModel.ReasonID==0) if (DisplayModel.OtherText == "")
{
DisplayMessage = "You must type a description of your issue.";
StateHasChanged();
await Task.Delay(1500);
DisplayMessage = "";
}
}
}
I found it surprisingly difficult to dynamically create and use Radio Buttons and Checkboxes in an asp.net Blazor (Server-side) page, mostly because:
I could not figure out how to bind in a <input type="radio"> field
I came across the EditForm rather late
I did not find (m)any examples
one has to use a loop-local variable to avoid closures (and index out of bound errors)
<InputCheckbox> seems to be unable to bind to a List<bool> and a list of complex objects is required and throws an ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. error.
So I am posting this question and answering it at the same time with a working solution. The following code first demonstrates the use of regular <input> fields and later uses the EditForm component. As far as I can tell, the latter one is the preferred solution.
I still have one question: How can one use a List<bool> instead of a list of complex objects? It just doesn't feel right having to use a separate class to encapsulate a singe property. It also complicates things if formModel is persisted using EF Core. The same issue was discussed in a similar context e.g. here.
I think your answer over complicates this.
The code below demonstrates a basic setup (it's demo code not production).
It uses the EditForm with a model. There are radio buttons and checkboxes linked into a model that get updated correctly. Selected has a setter so you can put a breakpoint in ans see who's being updated.
#page "/"
#using Blazor.Starter.Data
<EditForm EditContext="this.editContext">
#foreach (var model in models)
{
<h3>#model.Value</h3>
<h5>Check boxes</h5>
foreach (var option in model.Options)
{
<div>
<InputCheckbox #bind-Value="option.Selected" />#option.Value
</div>
}
<h5>Option Select</h5>
<div>
<InputRadioGroup #bind-Value="model.Selected">
#foreach (var option in model.Options)
{
<div>
<InputRadio Value="option.Value" /> #option.Value
</div>
}
</InputRadioGroup>
<div>
Selected: #model.Selected
</div>
</div>
}
</EditForm>
<button class="btn btn-dark" #onclick="OnClick">Check</button>
#code {
private EditContext editContext;
private List<Model> models;
protected override Task OnInitializedAsync()
{
models = Models;
editContext = new EditContext(models);
return Task.CompletedTask;
}
public void OnClick(MouseEventArgs e)
{
var x = true;
}
public List<Model> Models => new List<Model>()
{
new Model() { Value = "Fred"},
new Model() { Value = "Jon"},
};
public class ModelOptions
{
public string Value { get; set; }
public bool Selected
{
get => _Selected;
set
{
_Selected = value;
}
}
public bool _Selected;
}
public class Model
{
public string Value { get; set; }
public string Selected { get; set; }
public List<ModelOptions> Options { get; set; } = new List<ModelOptions>()
{
new ModelOptions() {Value="Tea", Selected=true},
new ModelOptions() {Value="Coffee", Selected=false},
new ModelOptions() {Value="Water", Selected=false},
};
}
}
Here's what it looks like:
Here is the code example mentioned in the question with more detailled comments. First the version without EditForm, which is not the recommended way, but nicely demonstrates the closure problem and why a loop-local variable is required. It also demonstrates my failure to use an <input> field of type="radiobutton" as it seems impossible to bind a value to it:
#page "/"
<h1>Use Checkboxes and Radio Buttons in asp.net Blazor w/o EditForm</h1>
<h2>Checkboxes using a for-loop and a List of bool</h2>
Don't tick the left column, as it generates an index out of bounds error.
#for (int i = 0; i < CheckboxList.Count(); i++)
{
// Gotcha: This is a closure, so the function call is stored together with the environment.
// Since i resides outside of the loop, it is a single variable which is stored with the function call, so
// index out of bound occurs because the last value is used. Instead, use a local variable, so for each
// iteration, a new variable is used and bound to the function. This is a compiler gotcha.
// Since C# 5.0, the loop variable of a foreach lop is inside the loop, so using foreach is save, but it
// does not work with a List<bool> but requires a List of objects than contains a bool as in
// foreach(bool items in CheckboxList), item is the iteration variable and cannot be assigned.
// https://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/
// https://stackoverflow.com/questions/58843339/getting-argumentoutofrangeexception-using-for-loop-in-blazor-component
int ii = i;
<div class="form-check">
<input type="checkbox" #bind=#CheckboxList[i] /><!-- does not work, index out of range -->
<input type="checkbox" #bind=#CheckboxList[ii] />
<label>Answer #ii</label>
</div>
}
Checkbox selection for-loop: #OutTextCheckboxFor
<hr />
<h2>Checkboxes using a foreach-loop and a List<Item></h2>
#foreach (CheckboxItem item in CheckboxItems)
{
<div class="form-check">
<input type="checkbox" #bind=#item.IsChecked />
<label>#item.Title</label>
</div>
}
Checkbox selection foreach-loop: #OutTextCheckboxForEach
<hr />
<h2> Does not work: Radio Buttons using a foreach-loop and a List<Item></h2>
Somehow it seems as if binding a radio button does not work the same way as binding a checkbox. I would expect
that item.Ischecked is true/false, depending on the selection of the radio button.
#foreach (RadioItemBool item in RadioItems)
{
<div class="form-check">
#* would need to add value=, but can't as it is already assigned by bind?? *#
<input type="radio" #bind=#item.IsChecked />
<label>#item.Title</label>
</div>
}
Radio selection foreach-loop: #OutTextRadioForEach
<hr />
<button #onclick="OnSubmit">
Evaluate all the above
</button>
<hr />
#code {
// could also use an array as a direct replacement for the list
// could also be a field instead of a property
public List<bool> CheckboxList { get; set; } = new List<bool> { true, false, true };
public List<CheckboxItem> CheckboxItems = new List<CheckboxItem>() {
new CheckboxItem(true, "Checkbox 1"), new CheckboxItem(false, "Checkbox 2"), new CheckboxItem(true, "Checkbox 3") };
public List<RadioItemBool> RadioItems = new()
{
new RadioItemBool(false, "Radio 1"),
new RadioItemBool(false, "Radio 2"),
new RadioItemBool(false, "Radio 3")
};
string OutTextCheckboxFor;
string OutTextCheckboxForEach;
string OutTextRadioForEach;
public class CheckboxItem
{
public bool IsChecked;
public string Title;
public CheckboxItem(bool isChecked, string title)
{
IsChecked = isChecked;
Title = title;
}
}
public class RadioItemBool
{
public bool IsChecked;
public string Title;
public RadioItemBool(bool isChecked, string title)
{
this.IsChecked = isChecked;
this.Title = title;
}
}
public void OnSubmit()
{
OutTextCheckboxFor = "";
for (int i = 0; i < CheckboxList.Count(); i++)
{
OutTextCheckboxFor += " " + (CheckboxList[i] ? "1" : "0");
}
OutTextCheckboxForEach = "";
foreach (CheckboxItem item in CheckboxItems)
{
OutTextCheckboxForEach += " " + (item.IsChecked ? "1" : "0");
}
OutTextRadioForEach = "";
foreach (RadioItemBool item in RadioItems)
{
OutTextRadioForEach += " " + (item.IsChecked);
}
}
}
And here is the preferred version with EditForm, also demonstrating data validation for a text input. It also shows that it seems impossible to bind the checkboxes to a list of simple types (List<bool>) and a list of a complex type (List<myBool>) is required to prevent the ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. exception (c.f. here). Often, the text shown in the checkboxes might also be stored in the separate class (myBool), but to demonstrate the shortcoming of not beeing able to bind to a list of bool, only the bool is inside a separte class.
#page "/"
#using System.ComponentModel.DataAnnotations
<h2>EditForm input with validation</h2>
#* https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation *#
<EditForm Model="#formModel" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group row">
<label for="ShortText" class="col-sm-2 col-form-label">Short Text</label>
<div class="col-sm-6">
<InputText #bind-Value="formModel.ShortText" class="form-control" id="ShortText" aria-describedby="ShortTextHelp"
placeholder="Enter short text" />
<small id="ShortTextHelp" class="form-text text-muted">This is a required field.</small>
</div>
</div>
<div class="form-group row">
<label for="Checkboxes" class="col-sm-2 col-form-label">Checkbox</label>
<div id="Checkboxes" class="col-sm-10">
#for (int i = 0; i < formModel.IsCheckedComplex.Count; i++)
{
// prevent closures (IsCheckedComplex[ii] needs to be a loop-local variable)
int ii = i;
<div class="col-sm-10">
#* Unfortunately, using #bind-value with a List<bool> does not work (see comment below *1) *#
<InputCheckbox class="form-check-input" id="#ii" #bind-Value="#formModel.IsCheckedComplex[ii].IsChecked" />
<label class="form-check-label" for=#ii>"Text for item"</label>
</div>
}
</div>
</div>
<div class="form-group row">
<label for="RadioButtons" class="col-sm-2 col-form-label">Radio buttons</label>
<div id="RadioButtons" class="col-sm-10">
<InputRadioGroup id="RadioButtons" #bind-Value=#formModel.SelectedRadio>
#foreach (var item in formModel.RadioItems)
{
<div class="col-sm-10">
<InputRadio class="form-check-input" id=#item.Index Value=#item.Index />
<label class="form-check-label" for=#item.Index>#item.Title</label>
</div>
}
</InputRadioGroup>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
<p>Inspect formModel in HandleValidSubmit() to see user inputs.</p>
</EditForm>
#code {
private FormModel formModel = new();
public class FormModel
{
public string Text { get; set; }
[Required]
[StringLength(10, ErrorMessage = "Short Text is too long.")]
public string ShortText { get; set; }
// === for the checkboxes
// unfortunately, binding to a List<bool> is not possible; try replacing IsCheckedComplex with IsChecked above *1
public List<bool> IsChecked { get; set; } = new() { true, false, false };
public List<myBool> IsCheckedComplex { get; set; } = new() { new(true), new(false), new(false) };
// a complex object is required as using List<bool> directly, throws an ArgumentException: The provided expression
// contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member
// accessors (fields, properties) of an object. See e.g. https://github.com/dotnet/aspnetcore/issues/12000
public class myBool
{
public bool IsChecked { get; set; }
// store the text shown in the checkbox items here as well
public myBool(bool init)
{
IsChecked = init;
}
}
// === for the radio buttons
public int SelectedRadio = 2;
public class RadioItem
{
public int Index;
public string Title;
public RadioItem(int index, string title)
{
this.Index = index;
this.Title = title;
}
}
public List<RadioItem> RadioItems = new()
{
new RadioItem(1, "Radio 1"),
new RadioItem(2, "Radio 2"),
new RadioItem(3, "Radio 3")
};
}
private void HandleValidSubmit()
{
// HandleValidSubmit called
}
}
I have the following IQueryable object:
var user = from d in _context.Users
join userRole in _context.UserRoles on d.Id equals userRole.UserId
join role in _context.Roles on userRole.RoleId equals role.Id
where role.Name == "Liquidador"
select d;
Which then is send as a ViewBag to the View:
ViewBag.UserID = new SelectList(user.AsNoTracking(), "UserName", "Name", selectedUser);
The Problem:
I need to add a new Item to the result of the IQueryable. So I've proceeded like this:
var UserNameList = user.Select(s => new { s.Name, s.UserName }).ToList();
However, I'm missing something when I'm trying to add the new item:
UserNameList.Insert(0, new *NewWhat?* { Name = "Liquidador", UserName = "--Select--"} );
Usually I declare a new element of a specific model but in this case I don't know which model to declare for this IQueryable. Any recomendations?
Thanks
EDIT:
The IQueryable object goes to the Get Method of the View as part of a function:
public async Task<IActionResult> Management()
{
PopulateUserDropDownList();
var Tiendas = await _context.Stores.ToListAsync();
StoreEmployee model = new StoreEmployee
{
Stores = Tiendas
};
return View(model);
}
This list is then presented in a dropdownlist, inside a table:
<td class="col-md-2">
<div class="form-group" form="#(String.Format("{0}{1}","form",item.StoreID))">
<div>
<select asp-for="#item.Usuario" class="form-control" asp-items="ViewBag.UserId" form="#(String.Format("{0}{1}","form",item.StoreID))"></select>
<span asp-validation-for="#item.Usuario" class="text-danger"></span>
</div>
</div>
</td>
It seems it would be cleaner if you define a class UserDTO (or any other name that likes you more)
public class UserDTO
{
public string Name { get; set; }
public string UserName { get; set; }
}
and then you do
var UserNameList = user
.Select(s => new UserDTO { Name = s.Name, UserName = s.UserName })
.ToList();
UserNameList.Insert(0, new UserDTO { Name = "Liquidador", UserName = "--Select--"} );
OTOH... smells a little to add the empty element as part of the data array, my recommendation is to handle that on razor view and just send UserNameList with real data. Dropdown razor methods contains overloads to specify the empty element text.
If you show your HTML, we may help you to implement a better solution.
Issues with custom workflow activity in CRM 2013 On-prem
I'm trying to pass the Manager of the System
here is the code that I'm running, it gets to setting the MANAGER and stops
I put the ran the FetchXML seperatly and it does return a value so I know what bit works
public class CaseAccountManagerManagersLookup : CodeActivity
{
// Inputs
[Input("Enter Case")]
[ReferenceTarget("incident")]
public InArgument<EntityReference> CA { get; set; }
// Outputs
[Output("Manager Output")]
[ReferenceTarget("systemuser")]
public OutArgument<EntityReference> AMOUT { get; set; }
protected override void Execute(CodeActivityContext executionContext)
{
// Context
IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
//Create the tracing service
ITracingService tracingService = executionContext.GetExtension<ITracingService>();
// get the account and renewals manager ID's
var CASE = CA.Get<EntityReference>(executionContext);
tracingService.Trace("Case ID = " + CASE.Id);
try
{
// FETCH
string fetchXml = string.Format(#"
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
<entity name='incident'>
<attribute name='title' />
<attribute name='incidentid' />
<order attribute='title' descending='false' />
<filter type='and'>
<condition attribute='incidentid' operator='eq' value='{0}' />
</filter>
<link-entity name='contact' from='contactid' to='customerid' alias='ak'>
<link-entity name='account' from='accountid' to='parentcustomerid' alias='al'>
<link-entity name='systemuser' from='systemuserid' to='bc_dssalesperson' alias='am'>
<attribute name='parentsystemuserid' />
</link-entity>
</link-entity>
</link-entity>
</entity>
</fetch>", CASE.Id);
EntityCollection case_results = service.RetrieveMultiple(new FetchExpression(fetchXml));
//tracingService.Trace("fetch has run");
if (case_results.Entities.Count != 0)
{
foreach (var a in case_results.Entities)
{
//if (a.Attributes.Contains("ai_parentsystemuserid"))
//{
tracingService.Trace("set manager id next");
var MANAGERID = (EntityReference)a.Attributes["parentsystemuserid"];
tracingService.Trace("manager id set");
AMOUT.Set(executionContext, MANAGERID);
throw new InvalidOperationException("Want to see trace");
//}
}
}
tracingService.Trace("end ");
}
catch (Exception e)
{
throw new InvalidPluginExecutionException("Plugin - CaseAccountManagerManagerLookup - " + e.Message);
}
finally
{
throw new InvalidOperationException("Want to see trace");
}
}
}
Try to use am.parentsystemuserid instead of just parentsystemuserid.
Are you sure that the guid that you are passing is in the correct form?
{8B8099A6-8B89-E411-883D-D89D676552A0}
this is what i get from the export but you are writing
8B8099A6-8B89-E411-883D-D89D676552A0
Also are you sure about the record that you are trying to retrieve? Is the chain complete with data?
case-> contact -> account -> parent user -> parent user ?
I am pretty new to ASP.Net MVC (and razor) and I have a few questions.
1)
I created an HTML extension to create a check box list as below:
public static HtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<InputItemInfo> ItemInfo)
{
if (String.IsNullOrEmpty(name))
throw new ArgumentException("The argument must have a value", "name");
if (ItemInfo == null)
throw new ArgumentNullException("ItemInfo");
if (ItemInfo.Count < 1)
throw new ArgumentException("The list must contain at least one value", "ItemInfo");
StringBuilder sb = new StringBuilder();
ItemInfo.Insert(0, new InputItemInfo("*", "Select All", ItemInfo.All(i => i.IsChecked)));
foreach (InputItemInfo info in ItemInfo)
{
TagBuilder builder = new TagBuilder("input");
if (info.IsChecked) builder.MergeAttribute("checked", "checked");
builder.MergeAttribute("type", "checkbox");
builder.MergeAttribute("value", info.Value);
builder.MergeAttribute("name", name);
builder.InnerHtml = info.DisplayText;
sb.Append(builder.ToString(TagRenderMode.Normal));
sb.Append("<br />");
}
return new HtmlString(sb.ToString());
}
I was able to use this in my views and also get the values in the controller as shown below:
#model List<AppTest.Models.InputExtensionsViewModel>
#{
ViewBag.Title = "Check";
}
<h2>Check</h2>
#using (Html.BeginForm())
{
<table border="0" style="border:0px;">
<tr>
<td valign="top">
#Html.Partial("CheckBoxList", Model[0])
</td>
</tr>
</table>
<br />
<input type="submit" value="Go" />
}
<div style="font-weight:bolder">
#ViewData["data"]
</div>
controller:
public ActionResult Check()
{
var model = new List<InputExtensionsViewModel>();
var model1 = new InputExtensionsViewModel
{
Title = "Facilities",
InputElementName = "facilities",
InputElements = // a list
};
model.Add(model1);
return View(model);
}
[HttpPost]
public ActionResult Check(string[] facilities)
{
...
}
The model is:
public class InputExtensionsViewModel
{
public string Title { get; set; }
public string InputElementName { get; set; }
public List<InputItemInfo> InputElements { get; set; }
public void SetSelected(string[] items)
{
if (items == null)
return;
this.InputElements.ForEach(delegate(InputItemInfo info)
{
if (items.Contains(info.Value))
info.IsChecked = true;
});
}
}
My question is, is there a way by which I could bind the array items to a property in the InputExtensionsViewModel model? If I just add a property called facilities to the view model, it's not bound automatically and I can understand why, as I am not binding that in my view. But I cannot seem to think of a way by which I could do that.
This check box list is a user control and I just wanted to avoid having too many string[] array for my action methods.
[EDIT] - Okay, I was able to do this when I tried now. Not sure why it didn't work before.
2) And, I was checking for alternatives and found out this answer in SO:
CheckboxList in MVC3.0
And I was able to replicate this but my question is, how do i bind a label to this checkbox? My labels are dynamic and part of the model and so cannot be hard-coded. I was trying to use a Html.LabelFor but that didn't work. In the editor template, if I just #Model.Text, it won't work and will be lost after a post-back as its not bound to a property
I googled and found suggestions to create HTML helpers which is what I did earlier (my 1st question is about that).
Please let me know if something is unclear. I could elaborate. Any input is appreciated!
Thanks in advance!
Ah, I found the solutions!
1) As indicated in my edit - adding a property with a similar name to a model and using it in the [HttpPost] enabled action method works fine. Guess last time I missed the getter and setters.
2) For this, in the editor template for MyViewModel, we just need to add this (** and **, needless to say, remove **!):
#model AppName.Models.MyViewModel
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.IsChecked) **#Model.Text
#Html.HiddenFor(x => x.Text)**
EDIT:
I have changed this template to do more. Now there is a label control and it is associated to the checkboxes through jquery as shown below.
#model EncorPlusTest.Infrastructure.InputItemInfo
#Html.HiddenFor(model => model.Value)
#Html.CheckBoxFor(model => model.IsChecked) <label for="">#Model.Text</label>
#Html.HiddenFor(model => model.Text)
<br />
Then in jquery:
$('input:checkbox').each(function () {
var lbl = $(this).next('input:hidden').next('label');
var forID = $(this).attr('id');
$(lbl).attr('for', forID);
});
Hope its helpful for others!
To answer part 2, you can easily add your label text as a property such as:
public class MyViewModel
{
public int Id { get; set; }
public bool IsChecked { get; set; }
public string Text { get; set; }
}
Then your template would look similar to this:
#model AppName.Models.MyViewModel
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.IsChecked)
#Html.LabelFor(x => x.Text)
The only downside to the above is that the label wouldn't be linked directly to the checkbox. You can accomplish this by doing something such as: CheckboxList in MVC3
Depending on the chance for re-usability, you can always create your own HtmlHelper as you were doing in the first part of this and wrap in the suggestions from the URL I pasted above.
You don't jQuery to solve this problem. If you wrap the input with a label you get the same behavior.
By the way, another option, instead of a editor template, is a HTML Helper. Take a look at this:
public static class HtmlHelperExtensions
{
#region CheckBoxList
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<SelectListItem> listInfo)
{
return htmlHelper.CheckBoxList(name, listInfo, ((IDictionary<string, object>)null));
}
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<SelectListItem> listInfo, object htmlAttributes)
{
return htmlHelper.CheckBoxList(name, listInfo, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
}
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<SelectListItem> selectListItems, IDictionary<string, object> htmlAttributes)
{
// Verify arguments
if (String.IsNullOrEmpty(name))
throw new ArgumentNullException("name", "Name cannot be null");
if (selectListItems == null)
throw new ArgumentNullException("selectList", "Select list cannot be null");
if (selectListItems.Count() < 1)
throw new ArgumentException("Select list must contain at least one value", "selectList");
// Define items
StringBuilder items = new StringBuilder();
int index = 0;
// Loop through items)
foreach (SelectListItem i in selectListItems)
{
// hidden value
TagBuilder hiddenValue = new TagBuilder("input");
hiddenValue.MergeAttribute("type", "hidden");
hiddenValue.MergeAttribute("value", i.Value);
hiddenValue.MergeAttribute("id", string.Format("{0}_{1}__Value", name, index));
hiddenValue.MergeAttribute("name", string.Format("{0}[{1}].Value", name, index));
// check box
TagBuilder checkbox = new TagBuilder("input");
if (i.Selected)
checkbox.MergeAttribute("checked", "checked");
checkbox.MergeAttribute("id", string.Format("{0}_{1}__Selected", name, index));
checkbox.MergeAttribute("name", string.Format("{0}[{1}].Selected", name, index));
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("value", "true");
// wrapper label
TagBuilder wrapperLabel = new TagBuilder("label");
wrapperLabel.InnerHtml = checkbox.ToString(TagRenderMode.SelfClosing);
wrapperLabel.InnerHtml += i.Text;
// hidden selected
TagBuilder hiddenSelected = new TagBuilder("input");
hiddenSelected.MergeAttribute("type", "hidden");
hiddenSelected.MergeAttribute("value", i.Selected.ToString().ToLower());
hiddenSelected.MergeAttribute("name", string.Format("{0}[{1}].Selected", name, index));
// label for checkbox
TagBuilder checkBoxLabel = new TagBuilder("label");
checkBoxLabel.MergeAttribute("for", checkbox.Attributes["id"]);
checkBoxLabel.MergeAttribute("id", string.Format("{0}_{1}__Text", name, index));
checkBoxLabel.MergeAttribute("name", string.Format("{0}[{1}].Text", name, index));
// hidden text
TagBuilder hiddenText = new TagBuilder("input");
hiddenText.MergeAttribute("type", "hidden");
hiddenText.MergeAttribute("value", i.Text);
hiddenText.MergeAttribute("id", string.Format("{0}_{1}__Text", name, index));
hiddenText.MergeAttribute("name", string.Format("{0}[{1}].Text", name, index));
// Add item
items.AppendLine(hiddenValue.ToString(TagRenderMode.SelfClosing));
items.AppendLine(wrapperLabel.ToString(TagRenderMode.Normal));
items.Append(hiddenSelected.ToString(TagRenderMode.SelfClosing));
items.AppendLine(hiddenText.ToString(TagRenderMode.SelfClosing));
items.AppendLine();
index++;
}
return MvcHtmlString.Create(items.ToString());
}
public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
return CheckBoxList(htmlHelper, name, metadata.Model as List<SelectListItem>);
}
#endregion
}