I'm learning MVC and am currently making a simple SCRUM tracking system as I go along.
The problem I'm having is that when an Ajax.ActionLink is clicked, I run the same ajax action once for every scrum card displayed on the page.
As you can see, I have 9 cards displayed and I get 9 identical GET requests. (The action link is actually the color wheel image in the lower right hand side of the card).
SingleCard.cshtml (View) - "ColorPicker" is the name of my action.
<script src="#Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
...
<div class="card_footer" id="card_footer_id_#(Model.ID)">
<div class="card_tags">
[Tag1] [Tag2] [Tag3]
</div>
<div class="card_colorwheel_icon">
#Ajax.ImageActionLink("../Content/Images/color_wheel.png", "Color Wheel", "ColorPicker", new { cardid = Model.ID }, new AjaxOptions { UpdateTargetId = "ColorPickerDisplay" })
</div>
</div>
The ImageActionLink is a helper I'm using, but it works exactly like the normal ActionLink
HomeController.cs (Controller)
public ActionResult ColorPicker(int cardid)
{
var currentcard = db.Cards.Single(x => x.ID == cardid);
var colors = new List<CardRGB>();
var cards = db.Cards.ToList();
foreach (var card in cards)
{
colors.Add(new CardRGB
{
CardId = card.ID,
Red = (int)card.BG_Red,
Blue = (int)card.BG_Blue,
Green = (int)card.BG_Green
});
}
// disctint
var model = new ColorPickerViewModel()
{
Colors = colors,
Red = (int) currentcard.BG_Red,
Green = (int) currentcard.BG_Green,
Blue = (int) currentcard.BG_Blue
};
return PartialView(model);
}
Does anyone know why this code is running once per card?
EDIT: As requested!
public static class ImageActionLinkHelper
{
public static MvcHtmlString ImageActionLink(
this AjaxHelper helper,
string imageUrl,
string altText,
string actionName,
object routeValues,
AjaxOptions ajaxOptions)
{
var builder = new TagBuilder("img");
builder.MergeAttribute("src", imageUrl);
builder.MergeAttribute("alt", altText);
builder.MergeAttribute("title", altText);
var link = helper.ActionLink("[replaceme]", actionName, routeValues, ajaxOptions);
var html = link.ToHtmlString().Replace("[replaceme]", builder.ToString(TagRenderMode.SelfClosing));
return new MvcHtmlString(html);
}
}
Check the HTML of your page. In your singlecard.cshtml there is the line:
<script src="#Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
For every card again you include the javascript, so it is included 9 times. Therefore 9 requests will be sent to the server.
Solution: put the script-include on page level, not on card level.
Related
I have a requirement to have different forms for different clients which can all be configured in the background (in the end in a database)
My initial idea is to create an object for "Form" which has a "Dictionary of FormItem" to describe the form fields.
I can then new up a dynamic form by doing the following (this would come from the database / service):
private Form GetFormData()
{
var dict = new Dictionary<string, FormItem>();
dict.Add("FirstName", new FormItem()
{
FieldType = Core.Web.FieldType.TextBox,
FieldName = "FirstName",
Label = "FieldFirstNameLabel",
Value = "FName"
});
dict.Add("LastName", new FormItem()
{
FieldType = Core.Web.FieldType.TextBox,
FieldName = "LastName",
Label = "FieldLastNameLabel",
Value = "LName"
});
dict.Add("Submit", new FormItem()
{
FieldType = Core.Web.FieldType.Submit,
FieldName = "Submit",
Label = null,
Value = "Submit"
});
var form = new Form()
{
Method = "Post",
Action = "Index",
FormItems = dict
};
return form;
}
Inside my Controller I can get the form data and pass that into the view
public IActionResult Index()
{
var formSetup = GetFormData(); // This will call into the service and get the form and the values
return View(formSetup);
}
Inside the view I call out to a HtmlHelper for each of the FormItems
#model Form
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#using FormsSpike.Core.Web
#{
ViewData["Title"] = "Home Page";
}
#using (Html.BeginForm(Model.Action, "Home", FormMethod.Post))
{
foreach (var item in Model.FormItems)
{
#Html.FieldFor(item);
}
}
Then when posting back I have to loop through the form variables and match them up again. This feels very old school I would expect would be done in a model binder of some sort.
[HttpPost]
public IActionResult Index(IFormCollection form)
{
var formSetup = GetFormData();
foreach (var formitem in form)
{
var submittedformItem = formitem;
if (formSetup.FormItems.Any(w => w.Key == submittedformItem.Key))
{
FormItem formItemTemp = formSetup.FormItems.Single(w => w.Key == submittedformItem.Key).Value;
formItemTemp.Value = submittedformItem.Value;
}
}
return View("Index", formSetup);
}
This I can then run through some mapping which would update the database in the background.
My problem is that this just feels wrong :o{
Also I have used a very simple HtmlHelper but I can't really use the standard htmlHelpers (such as LabelFor) to create the forms as there is no model to bind to..
public static HtmlString FieldFor(this IHtmlHelper html, KeyValuePair<string, FormItem> item)
{
string stringformat = "";
switch (item.Value.FieldType)
{
case FieldType.TextBox:
stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='text' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
break;
case FieldType.Number:
stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='number' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
break;
case FieldType.Submit:
stringformat = $"<input type='submit' name='{item.Key}' value='{item.Value.Value}'>";
break;
default:
break;
}
return new HtmlString(stringformat);
}
Also the validation will not work as the attributes (for example RequiredAttribute for RegExAttribute) are not there.
Am I having the wrong approach to this or is there a more defined way to complete forms like this?
Is there a way to create a dynamic ViewModel which could be created from the origional setup and still keep all the MVC richness?
You can do this using my FormFactory library.
By default it reflects against a view model to produce a PropertyVm[] array:
```
var vm = new MyFormViewModel
{
OperatingSystem = "IOS",
OperatingSystem_choices = new[]{"IOS", "Android",};
};
Html.PropertiesFor(vm).Render(Html);
```
but you can also create the properties programatically, so you could load settings from a database then create PropertyVm.
This is a snippet from a Linqpad script.
```
//import-package FormFactory
//import-package FormFactory.RazorGenerator
void Main()
{
var properties = new[]{
new PropertyVm(typeof(string), "username"){
DisplayName = "Username",
NotOptional = true,
},
new PropertyVm(typeof(string), "password"){
DisplayName = "Password",
NotOptional = true,
GetCustomAttributes = () => new object[]{ new DataTypeAttribute(DataType.Password) }
}
};
var html = FormFactory.RazorEngine.PropertyRenderExtension.Render(properties, new FormFactory.RazorEngine.RazorTemplateHtmlHelper());
Util.RawHtml(html.ToEncodedString()).Dump(); //Renders html for a username and password field.
}
```
Theres a demo site with examples of the various features you can set up (e.g. nested collections, autocomplete, datepickers etc.)
I'm going to put my solution here since I found this searching 'how to create a dynamic form in mvc core.' I did not want to use a 3rd party library.
Model:
public class IndexViewModel
{
public Dictionary<int, DetailTemplateItem> FormBody { get; set; }
public string EmailAddress { get; set; }
public string templateName { get; set; }
}
cshtml
<form asp-action="ProcessResultsDetails" asp-controller="home" method="post">
<div class="form-group">
<label asp-for=#Model.EmailAddress class="control-label"></label>
<input asp-for=#Model.EmailAddress class="form-control" />
</div>
#foreach (var key in Model.FormBody.Keys)
{
<div class="form-group">
<label asp-for="#Model.FormBody[key].Name" class="control-label">#Model.FormBody[key].Name</label>
<input asp-for="#Model.FormBody[key].Value" class="form-control" value="#Model.FormBody[key].Value"/>
<input type="hidden" asp-for="#Model.FormBody[key].Name"/>
</div>
}
<input type="hidden" asp-for="templateName" />
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
You can use JJMasterData, it can create dynamic forms from your tables at runtime or compile time. Supports both .NET 6 and .NET Framework 4.8.
After setting up the package, access /en-us/DataDictionary in your browser
Create a Data Dictionary adding your table name
Click on More, Get Scripts, Execute Stored Procedures and then click on Preview and check it out
To use your CRUD at runtime, go to en-us/MasterData/Form/Render/{YOUR_DICTIONARY}
To use your CRUD at a specific page or customize at compile time, follow the example below:
At your Controller:
public IActionResult Index(string dictionaryName)
{
var form = new JJFormView("YourDataDictionary");
form.FormElement.Title = "Example of compile time customization"
var runtimeField = new FormElementField();
runtimeField.Label = "Field Label";
runtimeField.Name = "FieldName";
runtimeField.DataType = FieldType.Text;
runtimeField.VisibleExpression = "exp:{pagestate}='INSERT'";
runtimeField.Component = FormComponent.Text;
runtimeField.DataBehavior = FieldBehavior.Virtual; //Virtual means the field does not exist in the database.
runtimeField.CssClass = "col-sm-4";
form.FormElement.Fields.Add(runtimeField);
return View(form);
}
At your View:
#using JJMasterData.Web.Extensions
#model JJFormView
#using (Html.BeginForm())
{
#Model.GetHtmlString()
}
#model Contoso.MvcApplication.ViewModels.QuizCompletedViewModel
<h2>Quiz 1</h2>
<form method="post">
#for (int i = 0; i < Model.Questions.Count; i++) {
#Html.EditorFor(model => model.Questions[i], "Questions/_MultipleChoiceAnswerView")
}
<div>
<p style="float: left;">Question #ViewData["CurrentNumber"] of #ViewData["TotalQuestions"]</p>
<input type="submit" value="Continue" style="float: right;" />
</div>
</form>
As you can see, I'm showing all the questions through the loop. But I really want to show question by question in the page. This is the a related post to implement it.
Hide current element and show the next one
If I don't include that JQuery feature, it looks like this
And the problem is, with the last JQUERY feature, I'm going to show only one question at a time, so I need to validate only that question, and that's the part I really have no idea.
I mean, suppose that I've already that JQUERY function, so when the user press CONTINUE, it has to validate if the current question is valid, but only this, not all.
What can I do?
UPDATE: The code where I create the radio buttons:
#using Contoso.MvcApplication.Extensions
#model Contoso.MvcApplication.ViewModels.MultipleChoiceQuestionViewModel
<div class="question-container">
<h5>#Model.Question.QuestionText</h5>
</div>
<div class="answer-container">
#Html.RadioButtonForSelectList(m => Model.Question.SelectedAnswer, Model.AnswerRadioList)
#Html.ValidationMessageFor(m => m.Question.SelectedAnswer)
</div>
And I'm using HtmlExtensions:
public static class HtmlExtensions
{
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues,
IDictionary<string, object> htmlAttributes)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var sb = new StringBuilder();
if (htmlAttributes == null)
{
htmlAttributes = new RouteValueDictionary();
}
if (!htmlAttributes.ContainsKey("id"))
{
htmlAttributes["id"] = null;
}
foreach (SelectListItem item in listOfValues)
{
var id = string.Format(
"{0}_{1}",
htmlHelper.ClientIdFor(expression),
item.Value
);
htmlAttributes["id"] = id;
var radio = htmlHelper.RadioButtonFor(expression, item.Value, htmlAttributes).ToHtmlString();
var labelId = htmlHelper.ClientIdFor(expression);
sb.AppendFormat(
"<div class='rad'>{0}<label for=\"{1}\">{2}</label></div>",
radio,
id,
HttpUtility.HtmlEncode(item.Text)
);
}
return MvcHtmlString.Create(sb.ToString());
}
}
Look at using something like the following. It uses the visible div as the current step and looks for invalid elements contained within.
This is a rough set of code and has not been tested but hopefully it might give you an idea on how to proce
// attach continue button handler
$("#continue).click(function ()
{
var $step = $(":visible"); // get current step
var validator = $("form").validate(); // obtain validator
var anyError = false;
//find any elements within the current step that are invalid.
$step.find("input").each(function ()
{
if (!validator.element(this)) { // validate every input element inside this step
anyError = true;
}
});
if (anyError)
return false; // exit if any error found
//in this case use class confirm (or use hidden field value for step number)
if ($step.next().hasClass("confirm")) { // is it confirmation?
// show confirmation asynchronously
$.post("/wizard/confirm", $("form").serialize(), function (r)
{
// inject response in confirmation step
$(".confirm").html(r);
});
}
//workout if this is the last step.
//if not go to next question
// if it is submit the form
if ($step.next().hasClass("complete")) { // is there any next step?
$step.hide().next().show(); // show it and hide current step
$("#back-step").show(); // recall to show backStep button
}
else { // this is last step, submit form
$("form").submit();
}
});
I am using a selector to build a custom #Html.EditorFor (called #Html.FullFieldEditor). It determines the type of input to generate (textbox, drop down, radio buttons, check boxes, etc.). I have been trying to hook it into one for a radio button list, thusly:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "myRadioButton" } })
or like this:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new { data_bind = "checked: myRadioButton" })
But with no luck.
I was trying to fix my selector.cshtml code but only wound up making a horrible mess. Here is the code that has worked for me BEFORE I was trying to implement knockout.js:
#{
var supportsMany = typeof (IEnumerable).IsAssignableFrom(ViewData.ModelMetadata.ModelType);
var selectorModel = (Selector)ViewData.ModelMetadata.AdditionalValues["SelectorModelMetadata"];
var fieldName = ViewData.TemplateInfo.GetFullHtmlFieldName("");
var validationClass = ViewData.ModelState.IsValidField(fieldName) ? "" : "input-validation-error";
// Loop through the items and make sure they are Selected if the value has been posted
if(Model != null)
{
foreach (var item in selectorModel.Items)
{
if (supportsMany)
{
var modelStateValue = GetModelStateValue<string[]>(Html, fieldName) ?? ((IEnumerable)Model).OfType<object>().Select(m => m.ToString());
item.Selected = modelStateValue.Contains(item.Value);
}
else
{
var modelStateValue = GetModelStateValue<string>(Html, fieldName);
if (modelStateValue != null)
{
item.Selected = modelStateValue.Equals(item.Value, StringComparison.OrdinalIgnoreCase);
}
else
{
Type modelType = Model.GetType();
if (modelType.IsEnum)
{
item.Selected = item.Value == Model.ToString();
}
}
}
}
}
}
#functions
{
public MvcHtmlString BuildInput(string fieldName,
SelectListItem item, string inputType, object htmlAttributes)
// UPDATE: Trying to do it above
{
var id = ViewData.TemplateInfo.GetFullHtmlFieldId(item.Value);
var wrapper = new TagBuilder("div");
wrapper.AddCssClass("selector-item");
var input = new TagBuilder("input");
input.MergeAttribute("type", inputType);
input.MergeAttribute("name", fieldName);
input.MergeAttribute("value", item.Value);
input.MergeAttribute("id", id);
input.MergeAttributes(new RouteValueDictionary(htmlAttributes));
// UPDATE: and trying above, but see below in the
// #foreach...#BuildInput section
input.MergeAttributes(Html.GetUnobtrusiveValidationAttributes(fieldName, ViewData.ModelMetadata));
if(item.Selected)
input.MergeAttribute("checked", "checked");
wrapper.InnerHtml += input.ToString(TagRenderMode.SelfClosing);
var label = new TagBuilder("label");
label.MergeAttribute("for", id);
label.InnerHtml = item.Text;
wrapper.InnerHtml += label;
return new MvcHtmlString(wrapper.ToString());
}
/// <summary>
/// Get the raw value from model state
/// </summary>
public static T GetModelStateValue<T>(HtmlHelper helper, string key)
{
ModelState modelState;
if (helper.ViewData.ModelState.TryGetValue(key, out modelState) && modelState.Value != null)
return (T)modelState.Value.ConvertTo(typeof(T), null);
return default(T);
}
}
#if (ViewData.ModelMetadata.IsReadOnly)
{
var readonlyText = selectorModel.Items.Where(i => i.Selected).ToDelimitedString(i => i.Text);
if (string.IsNullOrWhiteSpace(readonlyText))
{
readonlyText = selectorModel.OptionLabel ?? "Not Set";
}
#readonlyText
foreach (var item in selectorModel.Items.Where(i => i.Selected))
{
#Html.Hidden(fieldName, item.Value)
}
}
else
{
if (selectorModel.AllowMultipleSelection)
{
if (selectorModel.Items.Count() < selectorModel.BulkSelectionThreshold)
{
<div class="#validationClass">
#foreach (var item in selectorModel.Items)
{
#BuildInput(fieldName, item, "checkbox") // throwing error here if I leave this as is (needs 4 arguments)
//But if I do this:
//#BuildInput(fieldName, item, "checkbox", htmlAttributes) // I get does not exit in current context
}
</div>
}
else
{
#Html.ListBox("", selectorModel.Items)
}
}
else if (selectorModel.Items.Count() < selectorModel.BulkSelectionThreshold)
{
<div class="#validationClass">
#*#if (selectorModel.OptionLabel != null)
{
#BuildInput(fieldName, new SelectListItem { Text = selectorModel.OptionLabel, Value = "" }, "radio")
}*#
#foreach (var item in selectorModel.Items)
{
#BuildInput(fieldName, item, "radio")//same here
}
</div>
}
else
{
#Html.DropDownList("", selectorModel.Items, selectorModel.OptionLabel)
}
}
Any help is greatly appreciated.
EDIT
I am trying to minimize existing JS code (over 1500 lines) to show/hide with KO (which just seems to be able to cut down the code considerably). I know I've got choices (visible, if, etc.) with KO, but assuming what I wanted to accomplish with KO was doable I could go with visible. In any event, getting past the binding hurdle prevents me from getting that far.
Here is an example of some code I am using to show/hide with plain JS:
$(document).ready(function () {
$("input[name$='MyModel.MyRadioButton']").click(function () {
var radio_value = $(this).val();
if (radio_value == '1') {
$("#MyRadioButton_1").show();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
}
else if (radio_value == '2') {
$("#MyRadioButton_1").show();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
}
else if (radio_value == '3') {
$("#MyRadioButton_1").show();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
}
});
$("#MyRadioButton_1").hide();
$("#MyRadioButton_2").hide();
$("#MyRadioButton_3").hide();
});
I figured KO could minimize the above. Again, I'm looking at 20-30 inputs, most with more than 3 choices (a few have 10 choices in a Drop Down). This is getting hard to maintain at 1500 lines and growing.
And then in my view I've got this going on:
<div id="MyRadioButton_1">
#Helpers.StartingCost(MyModel.Choice1, "1")
</div>
<div id="MyRadioButton_2">
#Helpers.StartingCost(MyModel.Choice2, "2")
</div>
<div id="MyRadioButton_3">
#Helpers.StartingCost(MyModel.Choice2, "2")
</div>
The view code above will change slightly with KO, but again its the JS I am trying to cut down on.
EDIT 2
This is part of the code for FullFieldEditor. Some parts are left out for brevity (such as code for RequiredFor, ToolTipFor and SpacerFor).
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression)
{
return FullFieldEditor(html, expression, null);
}
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if (!metadata.ShowForEdit)
{
return MvcHtmlString.Empty;
}
if (metadata.HideSurroundingHtml)
{
return html.EditorFor(expression);
}
var wrapper = new TagBuilder("div");
wrapper.AddCssClass("field-wrapper");
var table = new TagBuilder("table");
table.Attributes["border"] = "0";
table.Attributes["width"] = "100%";//added this to even out table columns
var tbody = new TagBuilder("tbody");
var tr = new TagBuilder("tr");
var td1 = new TagBuilder("td");
td1.Attributes["width"] = "40%";
td1.Attributes["valign"] = "top";
var label = new TagBuilder("div");
label.AddCssClass("field-label");
label.AddCssClass("mylabelstyle");
label.InnerHtml += html.MyLabelFor(expression);
td1.InnerHtml = label.ToString();
var td2 = new TagBuilder("td");
td2.Attributes["width"] = "50%";
td2.Attributes["valign"] = "top";
var input = new TagBuilder("div");
input.AddCssClass("field-input");
input.InnerHtml += html.EditorFor(expression);
td2.InnerHtml = input.ToString();
var td3 = new TagBuilder("td");
td3.Attributes["width"] = "5%";
td3.Attributes["valign"] = "top";
if (metadata.IsRequired && !metadata.IsReadOnly)
{
td3.InnerHtml += html.RequiredFor(expression);
}
var td4 = new TagBuilder("td");
td4.Attributes["width"] = "5%";
td4.Attributes["valign"] = "middle";
if (!string.IsNullOrEmpty(metadata.Description))
{
td4.InnerHtml += html.TooltipFor(expression);
}
else td4.InnerHtml += html.SpacerFor(expression);
td4.InnerHtml += html.ValidationMessageFor(expression);
tr.InnerHtml = td1.ToString() + td2.ToString() + td3.ToString() + td4.ToString();
tbody.InnerHtml = tr.ToString();
table.InnerHtml = tbody.ToString();
wrapper.InnerHtml = table.ToString();
return new MvcHtmlString(wrapper + Environment.NewLine);
}
UPDATE 3
The options are not working. Option 1 will not even show data-bind in the <input>. Option 2 will not work since it's just checking if the field is required (the code just shows a "required" image if it is).
When I tried your first suggestion before your "UPDATE2" (input.MergeAttributes(new RouteValueDictionary(htmlAttributes));), this was the output:
<div class="field-input" data_bind="checked: MyRadioButton">
<div class="">
<div class="selector-item">
<input id="MyModel_MyRadioButton_Choice1" name="MyModel.MyRadioButton" type="radio" value="Choice1">
<label for="MyModel_MyRadioButton_Choice1">I am thinking about Choice 1.</label>
</div>
<!--Just showing one radio button for brevity-->
</div>
</div>
Since I merged the attribute with the input part of TagBuilder, which is outputting the field-input <div>, that is where it's being placed (which is logical). Notice that it should be data-bind but is showing as data_bind in the field-input class. This is how I have the FullFieldEditor:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "myRadioButton" } })
What it should be showing up as is this, I think:
<div class="field-input">
<div class="">
<div class="selector-item">
<!-- "data-bind" should be showing up in the following INPUT, correct?-->
<input data-bind="checked: MyRadioButton" id="MyModel_MyRadioButton_Choice1" name="MyModel.MyRadioButton" type="radio" value="Choice1">
<label for="MyModel_MyRadioButton_Choice1">I am thinking about Choice 1.</label>
</div>
<!--Just showing one radio button for brevity-->
</div>
</div>
What I suspect is that I have to get that htmlAttributes into the Selector.cshtml above, and not in the HtmlFormHelper.cs file. The Selector.cshtml is what is making the determination between showing, for example, a drop down list, a radio button list or a checkbox list (among others). The Selector.cshtml is a template in the Shared\EditorTemplates folder.
For background: I have dozens of forms representing hundreds of inputs over dozens of pages (or wizards). I am using the #Html.FullFieldEditor because it was easier to maintain than having spaghetti code for each type of input (drop down, checkbox, radio buttons, etc.).
UPDATE 4
Still not working.
I tried this in the Selector.cshtml (its the BuildInput function)code and was able to get "data-bind" into the <input> tag for each radio button in the list:
input.MergeAttribute("data-bind", htmlAttributes);
and then I did this lower down in the same file:
#foreach (var item in selectorModel.Items)
{
#BuildInput(fieldName, item, "radio", "test")
}
and my HTML output is this:
<div class="selector-item">
<input data-bind="test" id="MyModel_MyRadioButton_Choice1" name="MyModel.MyRadioButton" type="radio" value="Choice1">
<label for="MyModel_MyRadioButton_Choice1">Choice 1</label>
</div>
Which is what is leading me to believe it's the Selector.cshtml file, not the HtmlFormHelper.cs file.
I am going to open up the bounty to everyone 50+.
UPDATE3
First good call on the underscore bit, I totally forgot about that. Your bit of code looks almost right, but should actually be this:
#Html.FullFieldEditor(m => m.MyModel.MyRadioButton, new Dictionary<string, object> { { "data_bind", "checked:myRadioButton" } })
So since you are dynamically selecting between checkbox and text, you'll have to do a couple of things. If its a checkbox, you'll have to use the code above. If its a textbox, you'll have to use:
#Html.FullFieldEditor(m => m.MyModel.MyTextBox, new Dictionary<string, object> { { "data_bind", "value:MyTextBox" } })
UPDATE2
So I updated your code where I think the data-bind belongs in your html (marked option1 and option2). What would be helpful is if you gave me a snippet of the html being generated, along with where you need the data bind.
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression)
{
return FullFieldEditor(html, expression, null);
}
public static MvcHtmlString FullFieldEditor<T, TValue>(this HtmlHelper<T> html, Expression<Func<T, TValue>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if (!metadata.ShowForEdit)
{
return MvcHtmlString.Empty;
}
if (metadata.HideSurroundingHtml)
{
return html.EditorFor(expression);
}
var wrapper = new TagBuilder("div");
wrapper.AddCssClass("field-wrapper");
var table = new TagBuilder("table");
table.Attributes["border"] = "0";
//added this to even out table columns
table.Attributes["width"] = "100%";
var tbody = new TagBuilder("tbody");
var td1 = new TagBuilder("td");
td1.Attributes["width"] = "40%";
td1.Attributes["valign"] = "top";
var label = new TagBuilder("div");
label.AddCssClass("field-label");
label.AddCssClass("mylabelstyle");
label.InnerHtml += html.MyLabelFor(expression);
td1.InnerHtml = label.ToString();
var td2 = new TagBuilder("td");
td2.Attributes["width"] = "50%";
td2.Attributes["valign"] = "top";
var input = new TagBuilder("div");
input.AddCssClass("field-input");
// option1
input.InnerHtml += html.EditorFor(expression, htmlAttributes);
td2.InnerHtml = input.ToString();
var td3 = new TagBuilder("td");
td3.Attributes["width"] = "5%";
td3.Attributes["valign"] = "top";
if (metadata.IsRequired && !metadata.IsReadOnly)
{
// option2
td3.InnerHtml += html.RequiredFor(expression, htmlAttributes);
}
var td4 = new TagBuilder("td");
td4.Attributes["width"] = "5%";
td4.Attributes["valign"] = "middle";
if (!string.IsNullOrEmpty(metadata.Description))
{
td4.InnerHtml += html.TooltipFor(expression);
}
else
{
td4.InnerHtml += html.SpacerFor(expression);
}
td4.InnerHtml += html.ValidationMessageFor(expression);
var tr = new TagBuilder("tr");
tr.InnerHtml = td1.ToString() + td2.ToString() + td3.ToString() + td4.ToString();
tbody.InnerHtml = tr.ToString();
table.InnerHtml = tbody.ToString();
wrapper.InnerHtml = table.ToString();
return new MvcHtmlString(wrapper + Environment.NewLine);
}
UPDATE
While I still believe below is the "correct" answer, here is what your tagbuilder is missing so you can pass custom attributes along:
input.MergeAttributes(new RouteValueDictionary(htmlAttributes));
Original Answer
I have a feeling that what is going to happen by trying to mix razor and knockout, is the razor stuff will render, then when knockout is attached, the values in the knockout viewmodel are going to override whatever was in the razor view.
Here is my suggestion if you are trying to refactor to knockout:
Create an html only view (no razor).
Add your knockout bindings to it.
Pass your model data to knockout in the form of a json object using Json.NET. You can do this in a variety of ways, but the easiest being simply stuffing the data on the view inside of a <script> tag that your javascript can find.
Use Knockout Mapping to load the json object into the viewmodel.
Not sure I have understood your difficulty. But if your difficulty is where and how to place the data-bind depending of the input fields you used to render your property
give a look to the ClientBlocks features of the Mvc Controls Toolkit. It computes most of the binding automatically, by precompiling templates(that is razor helpers or PartialViews that display pieces of the page), or the full view.
That is you can just write: #Html.TextBoxFor(m => m.myProperty) and an adequate data-bind between the textbox and the myProperty of your client side ViewModel will be created, and so on with selects, radios etc. You can also to avoid using helper and just give your fileds "adequate names" and the binding is automatically computed. Moreover you can decide wichh part of the Server Side ViewModel to transfer to the client...your client side ViewmOdel will be transferred back inside the Server Side ViewModel automatically on post.
In brief: is it possible to track the number of times an Ajax.ActionLink method was called?
Now for context. I've got a simple model:
public class Person {
public string Name { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address {
public string City { get; set; }
public string Country { get; set; }
}
So, a person can have many addresses. On the Create page, I want the user to click a button that allows them to add as many Addresses as they want, dynamically.
I used this page as a reference in learning how to bind dynamically to a list: http://haacked.com/archive/2008/10/23/model-binding-toa-list.aspx,
With that as a reference, here are my classes:
HomeController:
//
// GET: /Home/
public ActionResult Index() {
return View();
}
[HttpPost]
public ActionResult Create(Person p) {
return View(p);
}
[OutputCache(NoStore = true, Duration = 0)]
public ActionResult AjaxAddAddress() {
TempData["key"] = DateTime.Now.Ticks.GetHashCode();
return PartialView("~/Views/Shared/EditorTemplates/Address.cshtml", new Address());
}
Index view:
#model ModelTest.Models.Person
<div>
#using (Html.BeginForm("Create", "Home")) {
<div>Name: #Html.TextBoxFor(m => m.Name)</div>
<div id="ajaxAddressBox"></div>
<p>#Ajax.ActionLink("Add Another Address", "AjaxAddAddress", new AjaxOptions {
UpdateTargetId = "ajaxAddressBox",
InsertionMode = InsertionMode.InsertAfter,
HttpMethod = "GET" })</p>
<input id="btnSubmit" type="submit" value="Create" />
}
</div>
Create View (just to confirm the model binded okay):
#model ModelTest.Models.Person
<div>
<p>You entered person: #Model.Name.</p>
<p>He has #Model.Addresses.Count total addresses.
#foreach (var c in Model.Addresses) {
<p>City: #c.City, Country: #c.Country</p>
}
</div>
Address editor template:
#model ModelTest.Models.Address
<p><input type="hidden" name="Addresses.Index" value="#TempData["key"]" />
City: #Html.TextBoxFor(x => x.City, new { Name = "Addresses[" + TempData["key"] + "].City" } )
Country: #Html.TextBoxFor(x => x.Country, new { Name = "Addresses[" + TempData["key"] + "].Country" })</p>
It seems to work ok, so I hope I'm doing this right so far. I'm new to MVC so please let me know if anything is totally wrong.
But I need it to do more. Ideally, I'd like to add a label that says "Address #(index)" for each line. But more important, I need to restrict the user to only adding, eg, 5 addresses. Either way, I'd like to track the number of times that Ajax.ActionLink, or the method AjaxAddAddress was called. Plus, in the future I'll need an edit page that also requires that restriction. Thus, if an existing person has 3 addresses, they can add only 2 more.
Any advice? It seems simple but I'm not sure how best to approach it. If I used a hidden field, how do you pass that value in Ajax.ActionLink and read it in my AjaxAddAddress method? Can you make a local client variable somehow?
I suppose a Session variable would work, but I always get nervous using that, not sure how long it lives or how reliable it is.
Here's one possible solution I came up with, with help from http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/.
Instead of using Ajax.ActionLink, I'm using Html.ActionLink and calling Ajax manually myself. That way, I can have it grab values right from Javascript. Whatever value I want, really: an expando, jquery.data, a hidden field, anything.
So my Ajax.ActionLink becomes:
#Html.ActionLink("Add Another Address", "AjaxAddAddress", null, new { id = "addItem" })
Then, within the same view I added this script:
<script type="text/javascript">
$("#addItem").click(function () {
$.ajax({
url: this.href + "?index=" + $("#ajaxAddressBox").children('div').size(),
cache: false,
success: function (html) {
$("#ajaxAddressBox").append(html);
}
});
return false;
});
</script>
I'm manually passing in an Index value to my AjaxAddAddresses method, and I'm basing that index value off the total number of div children currently in the ajaxAddressBox, or put another way, the current total number of addresses added. Thus, in the future when I build an Edit view, and it'll initially populate with existing addresses, this function will know how many addresses there are from the start.
AjaxAddAddresses becomes:
[OutputCache(NoStore = true, Duration = 0)]
public ActionResult AjaxAddAddress(int? index) {
if (index >= 5) return null;
TempData["key"] = DateTime.Now.Ticks.GetHashCode();
TempData["index"] = index + 1;
return PartialView("~/Views/Shared/EditorTemplates/Address.cshtml", new Address());
}
Thus, if the index is >= 5, I return null so that the user can't add more. (This could be done in the script block as well to save the wasted Ajax call, but at least when done server-side it can't be spoofed.)
And the Address Editor Template becomes:
#model ModelTest.Models.Address
<div><p><input type="hidden" name="Addresses.Index" value="#TempData["key"]" />
Address ##TempData["index"] ---
City: #Html.TextBoxFor(x => x.City, new { Name = "Addresses[" + TempData["key"] + "].City" } )
Country: #Html.TextBoxFor(x => x.Country, new { Name = "Addresses[" + TempData["key"] + "].Country" })</p></div>
Of course, other solutions are still welcome. This still feels needlessly complicated to me...
-ps, As it turns out, using a Session variable in my AjaxAddAddress method does work, but I can't shake the feeling that it could fail under some circumstances.
I believe that I couldn't find a proper title to explain my problem but I think this is the best possible short explanation.
Please let me explain the details.
I want to show a list of my pictures on a page and using a #foreach loop on MVC 3.
Partial View of this list as below:
#model IEnumerable<Picture>
#foreach (var item in Model)
{
<a href="#item.PicId">
<img height="35px" style="padding-top:3px" src="ImageHandler.ashx?id=#item.PicId" id="pictureMy" />
</a>
}
As you may understand I am sending a list to this partialview and it is placing the pictures on a single column.
It is working without any problem but I want to show 3 pictures for each row but couldn't manage.
Any guidance will be very appreciated.
Thanks in advance for your helps.
You could group them by 3:
#model IEnumerable<Picture>
#foreach (var item in Model.Select((value, index) => new { value, index }).GroupBy(x => x.index / 3))
{
<div>
#foreach (var picture in item)
{
<a href="#picture.value.PicId">
<img height="35px" style="padding-top:3px" src="ImageHandler.ashx?id=#picture.value.PicId" id="pictureMy" />
</a>
}
</div>
}
But honestly this grouping is not something that should be done in the view. You should define a view model and then have your controller action perform the grouping and return the view model.
So let's start by defining our view models:
public class PictureViewModel
{
public int PicId { get; set; }
}
public class GroupedPicturesViewModel
{
public IEnumerable<PictureViewModel> Pictures { get; set; }
}
then the controller action:
public ActionResult Index()
{
// fetch the pictures from the DAL or something
IEnumerable<Picture> pictures = ...
// Now build the view model
var model = pictures
.Select((value, index) => new { value, index })
.GroupBy(x => x.index / 3)
.Select(x => new GroupedPicturesViewModel
{
Pictures = x.Select(p => new PictureViewModel
{
PicId = p.value.PicId
})
}
);
return View(model);
}
then the corresponding view:
#model IEnumerable<GroupedPicturesViewModel>
#Html.DisplayForModel()
then the corresponding display template for the GroupedPicturesViewModel type (~/Views/Shared/DisplayTemplates/GroupedPicturesViewModel.cshtml):
#model GroupedPicturesViewModel
<div>
#Html.DisplayFor(x => x.Pictures)
</div>
and finally the display template for the PictureViewModel type (~/Views/Shared/DisplayTemplates/PictureViewModel.cshtml):
#model PictureViewModel
<a href="#Model.PicId">
<img class="image" src="#Url.Content("~/ImageHandler.ashx?id=" + Model.PicId)" alt="" />
</a>
One final thing that's bugging me is this anchor. Looks ugly. Don't you think? Looks like spaghetti code.
Let's improve it by writing a custom, reusable HTML helper which will render those pictures:
public static class HtmlExtensions
{
public static IHtmlString Picture(this HtmlHelper<PictureViewModel> htmlHelper)
{
var anchor = new TagBuilder("a");
var picture = htmlHelper.ViewData.Model;
var id = picture.PicId.ToString();
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
// You probably need another property on your view model here as this
// id is suspicious about href but since that's what you had in your
// original code I don't know what is your intent.
anchor.Attributes["href"] = id;
var image = new TagBuilder("img");
image.Attributes["alt"] = "";
image.Attributes["src"] = urlHelper.Content(
"~/ImageHandler.ashx?id=" + urlHelper.Encode(id)
);
image.AddCssClass("image");
anchor.InnerHtml = image.ToString();
return new HtmlString(anchor.ToString());
}
}
and then in the display template we will simply have:
#model PictureViewModel
#Html.Picture()
And that's pretty much it. No need to write loops. Everything works by convention.