#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();
}
});
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()
}
I am working on developing on a voting page mechanism. Here I will have the List of questions and against each question I have 3 options(I am using radio buttons). I have attached my View and Controller method. I am getting the value saved to DB correctly, but my problem is I am able to select multiple options where radio buttons are used. I want to make sure that, if one option is selected for a question the other options must be automatically deselected, which is not happening for me.
My View :
#using (Html.BeginForm())
{
<div>
#foreach (var a in ViewBag.Questions)
{
<h4>#a.Questions</h4>
<div>
#foreach (var b in Model)
{
if (b.QuestionsID == a.id)
{
#Html.RadioButton(b.AnswersOptions,
new {Answerid= b.id, Questionid=a.id })
#b.AnswersOptions
}
}
</div>
}
</div>
<br/>
<div >
<input type="submit" value="Vote Now!!"
onclick="return confirm('Are you sure you want to
submit your choices?');"/>
</div>
}
My Controller :
public ActionResult VotingResult_Post(FormCollection resultcollection)
{
int resultcollectionCount = resultcollection.Count;
if (resultcollectionCount == CountofQuestionsDisplayed)
{
for (int i = 0; i < resultcollectionCount; i++)
{
string SelectedIDArray = resultcollection[i];
string SelectedAnswerIDValue = GetValue("Answerid", SelectedIDArray);
string SelectedQuestionID = GetValue("Questionid", SelectedIDArray);
InsertUsersReponses(SelectedQuestionID, SelectedAnswerIDValue);
}
}
List<Voting_Questions> QuesList = PopulateQuestions();
ViewBag.Questions = QuesList;
List<Voting_Answers> Answers = aobj.Voting_Answers.ToList();
return View(Answers);
}
You need an HTML helper like the following
public static System.Web.Mvc.MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var sb = new StringBuilder();
string ForFormat = String.Empty;
if (listOfValues != null)
{
// Create a radio button for each item in the list
// need to create correct ID here
var baseID = metaData.PropertyName;
foreach (SelectListItem item in listOfValues)
{
// Generate an id to be given to the radio button field
var id = string.Format("{0}_{1}", baseID, item.Value);
// Create and populate a radio button using the existing html helpers
var label = htmlHelper.Label(id, HttpUtility.HtmlEncode(item.Text));
// extracting the text for="##" from the label and using this for the control ID
// ASSUMES the format<label for="TestRadio_1">Line 1</label> and splitting on the quote means that the required value is in the second cell of the array
String[] temp = label.ToString().Split('"');
var radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = temp[1] }).ToHtmlString();
// Create the html string that will be returned to the client
// e.g. <input data-val="true" data-val-required="Option1" id="TestRadio_1" name="TestRadio" type="radio" value="1" /><label for="TestRadio_1">Line 1</label>
// e.g. <input data-val="true" data-val-required="Option2" id="TestRadio_2" name="TestRadio" type="radio" value="2" /><label for="TestRadio_2">Line 2</label>
sb.AppendFormat("<div class=\"RadioButtonList\">{0}{1}</div>", radio, label);
}
}
return MvcHtmlString.Create(sb.ToString());
which is called as follows:
#Html.ValidationMessageFor(m => m.myProperty)
#Html.LabelFor(m => m.myProperty, new { #class = "editor-label control-label" })
<div class="editor-field controls radio">
#Html.RadioButtonForSelectList(
m => m.myProperty,
ListOfOptionsForRadioButton
)
</div>
The markup is for Bootstrap.
i am working on mvc project. In my controller i am calling my stored procedure from terms class and I am returning Index page if it returns true or return terms page if it returns false.
Calling stored procedure in terms page :
public class Accept
{
public void Check()
{
using (var ctx = new termsEntities())
{
ctx.usp_ChkTerms(8, new ObjectParameter("Accepted", typeof(bool)));
ctx.SaveChanges();
}
}
}
Now i am calling this in my controller :
public ActionResult App()
{
// calling Stored procedure from Model to class
var accept = new Accept();
accept.Check();
// checking if accepted is true then return view else return another view
AppEntities Accepted = new AppEntities();
AppTerm user = new AppTerm();
AppHist history = new AppHist();
user = (from AppTerm app in Accepted.AppTerms
where app.userID == 8
select app).ToList().FirstOrDefault();
if (user != null)
{
if (user.Accepted)
{
return View("Index");
}
else
{
return View("terms");
}
}
And this is the code i am using in my terms view :
#{
ViewBag.Title = "terms";
}
<html>
<body>
<ul>
#foreach ( var item in Model)
{
<div class="Page" onclick="location.href='#Url.Action("Info", new { id = item.ID })'">
span class="Col1">
<br />
#item.ID
</span>
<span class="Title">#item.Name</span>
}
</ul>
</body>
</html>
Here when condition is true it is displaying Index page but when condition falls and when it tries to display terms page i am getting Object reference not set to an instance of an object and error is pointing to foreach loop. so what mistake i am doing here? i need help..
It is ugly, but you may try
<div class="Page" onclick='location.href="#Url.Action("Info", new { id = item.ID })"'>
<div class="Page" onclick="location.href='#Url.Action("Info", new { id = item.ID })'">
Change this to:
<div class="Page" onclick="location.href='#Url.Action('LinkText','Info', new { id = item.ID })'">
Note the quote marks around Info
edit:
Added extra argument to link.
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.
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.