I can figure out why it's not binding. So I have a form where a ListBox is in a partial view which I reload everytime I click on a checkbox to fill the listbox.
The code of my ModelView for the form is :
<div class="row-fluid">
<div class="span3">
<label>Fonction(s):</label>
</div>
<div class="span9" id="ListeFonction">
#Html.Partial("ListerFonction", Model)
</div>
</div>
<div class="row-fluid">
<div class="span5 offset3">
<div class="fonctions_container">
#foreach (extranetClient.Models.Classes.FonctionContact fonction in ViewBag.Fonctions)
{
string coche = "";
if ((#Model.ListeFonctions).Any(c => c.IdFonction == fonction.IdFonction))
{
coche = "checked";
}
<input type="checkbox" #coche class="checkbox" value="#fonction.IdFonction" />#fonction.LibelleFonction <br />
}
</div>
</div>
</div>
So as you can see, I render a partial view just after the "Email" Textbox. The code for it is :
#Html.LabelFor(contact => contact.SelectedFonctionIds, "ListeFonctions")
#Html.ListBoxFor(contact => contact.SelectedFonctionIds, new MultiSelectList(Model.ListeFonctions, "IdFonction", "LibelleFonction"), new { disabled = "disabled")
The model associated to that view looks like that:
private List<int> _selectedFonctionIds;
public List<int> SelectedFonctionIds
{
get
{
return _selectedFonctionIds ?? new List<int>();
}
set
{
_selectedFonctionIds = value;
}
}
public List<FonctionContact> ListeFonctions = new List<FonctionContact>();
public MultiSelectList ListeFonctionsSelectList
{
get
{
return new MultiSelectList(
ListeFonctions,
"IdFonction", // dataValueField
"LibelleFonction" // dataTextField
);
}
}
public Contact() { }
public Contact( List<FonctionContact> listeFonctions, List<int> selectedFonctionIds)
{
this.ListeFonctions = listeFonctions;
this.SelectedFonctionIds = selectedFonctionIds;
}
public Contact(int idContact, string nom, string prenom, string email, string telephoneFixe, string telephonePort) {
this.IdContact = idContact;
this.Nom = nom;
this.Prenom = prenom;
this.Email = email;
this.TelephoneFixe = telephoneFixe;
this.TelephonePort = telephonePort;
}
public Contact(int idContact, string nom, string prenom, List<int> selectedFonctionIds, List<FonctionContact> listeFonctions, string email, string telephoneFixe, string telephonePort)
{
this.IdContact = idContact;
this.Nom = nom;
this.Prenom = prenom;
this.SelectedFonctionIds = selectedFonctionIds;
this.ListeFonctions = listeFonctions;
this.Email = email;
this.TelephoneFixe = telephoneFixe;
this.TelephonePort = telephonePort;
}
But the ListBox of the partial view is not binding with the model. I get well the other informations but not these in the listbox. Somebody has an idea ?
Why are you forcing the ListBox's id here:
#Html.ListBoxFor(contact => contact.SelectedFonctionIds,
new MultiSelectList(Model.ListeFonctions, "IdFonction", "LibelleFonction"),
new { disabled = "disabled", **id="idFonctions"** })
ListBoxFor helper is supposed to generate the ListBox's id for you, and the Id should be the same as the attribute it should bind with. Shouldn't it be SelectedFonctionIds?
Was the binding working before you started using the PartialView? Because from your previous question, I see that you had:
#Html.ListBoxFor(contact => contact.SelectedFonctionIds, Model.ListeFonctionsSelectList, new { disabled = "disabled" })
in your View (i.e., you didn't set the id attribute).
Related
In my .razor page I have an InputText, what I want is to update that number as soon as it is being typed, specifically is to put a space every 4 characters, how am I trying to do it?
<InputText #bind-Value="oPagos.NumeroEnTarjeta" #onkeydown="#Tecleando" type="number"
onchange="()=>NumberChanged()" id="card-number" placeholder="1111 2222 3333 4444" class="input" maxlength="16" />
Then,
public void Tecleando(KeyboardEventArgs e)
{
//Console.WriteLine(e.Key);
oPagos.NumeroEnTarjeta = generateSpaces(oPagos.NumeroEnTarjeta);
Console.WriteLine(oPagos.NumeroEnTarjeta);
}
I have a function where I plan to take all the value from the bind, ie: oPayments.NumberOnCard, and every 4 spaces generate a space.
This does not work for me for two reasons.
the first number that I type is taken from the #Onkeydown event but the variable oPagos.NumeroEnTarjeta is empty.
I don't know how to update the value of the InputText, as I show in the following image I effectively modify the variable oPagos.NumeroEnTarjeta, but I can't get the user to see it rendered in the text box.
Should I take another way or how do I fix what I have? Thank you.
Update
I succeeded in doing something similar, but with two different events, onblur and onfocus.
I use onfocus to remove the spaces and I use onblur to add my spaces, however, what I would like to do is while I'm writing
I got some Problems with Dynamic Data using Bind-Value / Bind so i started using Blazorise and solve my problems, a possible solution is this one:
<Field>
<TextEdit Text="#opagos.NumeroEnTarjeta" TextChanged="#MethodThatBringSpaces"></TextEdit>
<Field>
Then in #code
Task MethodThatBringSpaces(string value){
opagos.NumeroEnTarjeta = generateSpaces(value);
}
Also you can use the data that you want (i use string in this case) and you can add the same things than blazor (id,placeholder,etc.)
Here's a set of code which I think does basically what you want. It was written to answer a similar question on here a few months ago! I've used dashes instead of spaces to show the space being filled. It's was coded in Net6.0 but should be Ok in Net5.0.
You will probably need to tweak it a little to fit your exact needs:
CreditCardCode.razor
#namespace StackOverflowAnswers.Components
#inherits InputBase<string>
<input #attributes="AdditionalAttributes"
class="#CssClass"
value="#stringValue"
#oninput="OnInput"
#onchange="this.OnValueChanged"
#onfocus="OnFocus"
#onblur="OnBlur"
/>
CreditCardCode.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using System.Text.RegularExpressions;
namespace StackOverflowAnswers.Components;
public partial class CreditCardCode : InputBase<string>
{
private string stringValue = String.Empty;
private string _currentValue = String.Empty;
// Sets up the initial value of the input
protected override void OnInitialized()
{
_currentValue = this.Value ?? string.Empty;
this.stringValue = this.GetCodeValue(_currentValue);
base.OnInitialized();
}
private async Task OnInput(ChangeEventArgs e)
{
var rawValue = e.Value?.ToString();
stringValue = "";
await Task.Yield();
_currentValue = this.GetCodeValue(rawValue ?? string.Empty);
this.stringValue = this.FormatValueAsString(_currentValue);
}
private async Task OnFocus(FocusEventArgs e)
{
stringValue = "";
await Task.Yield();
this.stringValue = this.FormatValueAsString(_currentValue);
}
private async Task OnBlur(FocusEventArgs e)
{
stringValue = "";
await Task.Yield();
this.stringValue = this.GetCodeValue(_currentValue);
}
// We set the base CurrentValueAsString to let it handle all the EditContext changes and validation process
private void OnValueChanged(ChangeEventArgs e)
=> this.CurrentValueAsString = e.Value?.ToString() ?? string.Empty;
// Necessary override for InputBase
protected override bool TryParseValueFromString(string? value, out string result, out string validationErrorMessage)
{
result = value ?? string.Empty;
if (!string.IsNullOrEmpty(value) && value.Length == 19)
{
validationErrorMessage = string.Empty;
return true;
}
else
{
validationErrorMessage = "Value must be nnnn-nnnn-nnnn-nnnn";
return false;
}
}
protected override string FormatValueAsString(string? value)
=> value ?? string.Empty;
private string GetCodeValue(string value)
{
value = new string(value.Where(c => char.IsDigit(c)).ToArray());
value = value.Length > 16
? value.Substring(0, 16)
: value;
var reg = new Regex(#"([0-9]{1,4})");
var matches = reg.Matches(value);
var outvalue = string.Empty;
if (matches.Count > 0)
{
foreach (Match match in matches)
{
outvalue = $"{outvalue}-{match.Value}";
}
outvalue = outvalue.Trim('-');
return outvalue;
}
return string.Empty;
}
}
Test Page
#page "/"
#using StackOverflowAnswers.Components
<h3>EditForm</h3>
<div class="container-fluid">
<EditForm EditContext=editContext>
<div class="row">
<div class="col-2">
Credit Card No:
</div>
<div class="col-4">
<CreditCardCode class="form-control" #bind-Value="this.model.CreditCardNo"/>
</div>
<div class="col-4">
<ValidationMessage For="() => this.model.CreditCardNo" />
</div>
</div>
</EditForm>
<div class="row">
<div class="col-2">
Credit Card No:
</div>
<div class="col-4">
#model.CreditCardNo
</div>
</div>
</div>
#code {
private EditContext? editContext;
private ModelData model = new ModelData();
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(model);
return Task.CompletedTask;
}
class ModelData
{
public string CreditCardNo { get; set; } = string.Empty;
}
}
So, I am sending a dictionary object to my view from the controller.
// GET: QuestionResponses/Create
public ActionResult Create(int questionnaireUID)
{
var questions = from q in db.QUESTIONS
where q.QuestionnaireUID == questionnaireUID
select q;
ViewBag.NextQuestion = from q in db.QUESTIONS
where q.QuestionnaireUID == questionnaireUID
select new SelectListItem
{
Selected = (q.QuestionnaireUID == questionnaireUID),
Text = q.Question1,
Value = q.QuestionUID.ToString()
};
Dictionary<QUESTION, QUESTION_RESPONSES> dict = new Dictionary<QUESTION, QUESTION_RESPONSES>();
foreach (var question in questions)
{
dict.Add(question, new QUESTION_RESPONSES { QuestionUID = question.QuestionUID, Response = "", NextQuestion = "" });
}
return View(dict);
}
The reasoning behind this is that I need to view data from one model and need to add/edit data from another model. I tried using Tuples and was not able to get it to work (if you could tell me how to do this with Tuples, that would be helpful too).
This is what the view does with this Dictionary object.
<div class="form-group">
<h2> Reponses </h2>
<p> For each question, enter in the appropriate response(s). All questions must have at least one response. <p>
<div id="editorRows">
<div class="rows_no_scroll">
#foreach (var item in Model.ToArray())
{
<!-- The question that responses are being added to. -->
Html.RenderPartial("QuestionRow", item.Key);
<!-- All questions pertaining to this questionnaire. -->
// <p>Please select the question which should be asked as a response to this question.</p>
#Html.DropDownList("NextQuestion", null, htmlAttributes: new { #class = "form-control", id = "ddl_questions_" + count})
#Html.ValidationMessageFor(model => item.Value.NextQuestion, "", new { #class = "text-danger" })
<!-- The next question link and responses being inputted by user. -->
Html.RenderPartial("ResponseEditorRow", item.Value);
// <p> Question #count </p>
count += 1;
}
</div> <!--/rows_no_scroll-->
</div> <!-- /editorRows -->
</div> <!-- /form-group -->
For completeness, here are what the partial views are doing.
QuestionRow:
<div class="questionRow">
<!-- Hide attribute(s) not being viewed/edited. -->
#Html.HiddenFor(model => model.QuestionUID)
#Html.HiddenFor(model => model.QuestionnaireUID)
<!-- Show attribute(s) being viewed. -->
#Html.DisplayFor(model => model.Question1)
<div class="addQuestion">Add Response</div>
</div>
ResponseEditorRow:
<div class="editorRow">
#using (Html.BeginCollectionItem("questions"))
{
<!-- Hide attribute(s) not being viewed/edited. -->
#Html.HiddenFor(model => model.ResponseUID)
#Html.HiddenFor(model => model.QuestionUID)
<br>
<!-- Display attribute(s) being edited. -->
#Html.EditorFor(model => model.Response, new { htmlAttributes = new { #type = "text", #name = "question", #class = "question_input" } })
#Html.ValidationMessageFor(model => model.Response, "", new { #class = "text-danger" })
<input type="button" name="addRow[]" class="deleteRow" value="Delete">
}
</div>
The problem that I am having is that when I get back to my controller to POST the data inserted by the user, my Dictionary is empty. I'm not sure if I am inserting the information correctly. I am changing the dictionary object toArray(), not sure if this is affecting anything...
Here is the HTTP POST create method:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ResponseUID, QuestionUID, Response, NextQuestion")] Dictionary<QUESTION, QUESTION_RESPONSES> question_question_response)
{
if (ModelState.IsValid)
{
foreach (var item in question_question_response.ToArray())
{
db.QUESTION_RESPONSES.Add(item.Value);
db.SaveChanges();
return RedirectToAction("Index"); // Update to take user to Actions/Create page.
}
}
ViewBag.NextQuestion = new SelectList(db.QUESTIONS, "QuestionUID", "Question1");
return View(question_question_response);
}
Open to any suggestions of different ways I could do this or on what may be wrong with what I am currently doing.
Create 2 ViewModels:
public class QuestionAireViewModel {
public int QuestionAireId {get;set;}
public List<QuestionViewModel> Quesitons {get;set;}
}
public class QuestionViewModel{
public int QuestionId {get;set;}
public string Question {get;set;}
public string QuestionResponse {get;set;}
}
In your view pass this QuestionAireViewModel
Generate QuestionAireViewModel in your controller like this:
public ActionResult GetQuestions(int id)
{
var questionAire = db.QuesitonAire.First(s => s.QuestionAireId == id)
var questions = new List<QuestionViewModel>();
foreach(var question in questionAire.Questions){
questions.Add(new QuestionViewModel(){
Quesiton = question.Question,
});
}
var model = new QuestionAireViewModel(){
QuestionAireId = questionAire.Id,
Quesitons = questions
};
return View(model);
}
Then on POST Form method will be:
[HttpPost]
public ActionResult SaveQuestions(QuestionAireViewModel model)
{
}
How can I create dynamic ajax.actionlinks that will call dynamic partial views.
For example:
I have a page that will generate x number of comments
Each comment can be voted up or down (individually)
The number of up votes and down votes are counted into a single integer
Each comment div will have its own ajax.actionlink
Each ajax.actionlink will pass to the controller the ID of the comment
The controller will calculate the total votes and call the partial view to display back into the div with the correct ID.
What have I done so far:
I have been able to create successful ajax.actionlink
That will call a controller and sum the votes
That will call the partial view and display the votes
What is the issue
I don't want to hard code 30-100 different ajax.actionlinks to call 30-100 hard coded partial views.
How can I accomplish this dynamically?
Existing Code:
My ajax.actionlink inside my razor view
#Html.Raw(Ajax.ActionLink("[replacetext]", "VoteUp",
new { UserPostID = #Model.Id },
new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
"<img src=\"/Images/up_32x32.png\" />"))
My div inside the same razor view to display the returning results from the partial view.
<div id="CountVote" class="postvotes"></div>
My controller
public PartialViewResult VoteUp(int UserPostID)
{
try
{
UserVotes vote = new UserVotes();
vote.SubmitedVote = 1;
vote.UserId = Convert.ToInt32(Session["id"]);
vote.UserPostID = UserPostID;
ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);
}
catch (Exception e)
{
xxx.xxx.xxxx().Raise(e);
}
return PartialView("_TotalVotes");
}
And finally my partial view (_TotalVotes.cshtml)
#ViewBag.SumVotes
Now my main view for Viewpost shows the comments in a loop using the viewbag.
foreach (var item in (List<UserComment>)ViewData["Comments"])
{
CommentVote = "cv" + i.ToString();
<div class="postlinewrapper">
<div class="postvotesframe">
<div class="postvotes">
#Html.Raw(Ajax.ActionLink("[replacetext]", "VoteUp",
new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
"<img src=\"/Images/up_32x32.png\" />"))
</div>
<div id="#CommentVote" class="#CommentVote">0</div>
<div class="postvotes">
#Html.Raw(Ajax.ActionLink("[replacetext]", "VoteDown",
new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
"<img src=\"/Images/down_32x32.png\" />"))
</div>
</div>
<div class="postleftbar">
#Html.Raw(item.Comment)
</div>
<div class="postrightbar">
<div>
<div class="post_spec">
<div class="post_spec_title">Call Sign: </div>
<div class="post_spec_detail">#item.CallSign</div>
</div>
<div class="post_spec">
<div class="post_spec_title">When: </div>
<div class="post_spec_detail">#item.CommentDate.ToString("dd/MM/yyyy")</div>
</div>
</div>
<br />
<br />
</div>
</div>
i += 1;
}
I have implemented the login to increase or decrease votes up and down:
public PartialViewResult VoteUp(int userPostId)
{
try
{
UserVotes vote = new UserVotes();
vote.SubmitedVote = 1;
vote.UserId = Convert.ToInt32(Session["id"]);
vote.UserPostID = userPostId;
ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);
}
catch (Exception e)
{
xxxx.xxxx.xxxx().Raise(e);
}
return PartialView("_TotalVotes");
}
public PartialViewResult VoteDown(int userPostId)
{
try
{
UserVotes vote = new UserVotes();
vote.SubmitedVote = -1;
vote.UserId = Convert.ToInt32(Session["id"]);
vote.UserPostID = userPostId;
ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);
}
catch (Exception e)
{
xxx.xxxx.xxxx().Raise(e);
}
return PartialView("_TotalVotes");
}
Now all this code works for 1 ajax call just fine, but what I need to is to display separate ajax calls for separate divs dynamically.
Try it this way.
Main view
I'm supposing you have a model with a collection property Comments of Comment items
#model MyNamespace.CommentAndOtherStuff
<ul>
#foreach(item in Model.Comments)
{
<li>
<a href="#Url.Action("VoteUp", "VoteControllerName", new { UserPostId = item.Id })"
class="vote-link"
data-id="#item.Id">#item.Votes</a><img src="vote.jpg" />
</li>
}
</ul>
And your controller just returns a class called VoteResult as JSON.
[HttpPost]
public ActionResult VoteUp(int UserPostID)
{
...
var model = new VoteResult
{
UserPostID = UserPostID,
Votes = service.tallyVote(UserPostID)
};
return Json(model);
}
Now hook all of those up with a jQuery event handler and setup an AJAX call
$(document).ready(function() {
$("a.vote-link").on("click", function(event) {
event.preventDefault();
var link = $(this); // the link instance that was clicked
var id = link.attr("data-id");
var url = link.attr("href");
$.ajax({
url: url,
type: "post"
})
.done(function(result) {
// JSON result: { UserPostID: 1, Votes: 5 }
// replace link text
link.html(result.Votes);
});
});
});
But I want a partial view html fagment.
[HttpPost]
public ActionResult VoteUp(int UserPostID)
{
...
var model = new VoteResult
{
UserPostID = UserPostID,
Votes = service.tallyVote(UserPostID)
};
return PartialView("_TotalVotes", model);
}
_TotalVotes partial
#model MyNamespace.VoteResult
#if (Model.Votes < 0)
{
<span class="unpopular">#Model.Votes</span>
}
else
{
<span class="awesome">#Model.Votes</span>
}
And adjust the AJAX callback
.done(function(result) {
link.html(result);
});
Now you could write a helper for the link fragment but it obfuscates things in my opinion (it's a judgement call). All you really need here is the class name and the data-id which your javascript will bind.
Using the Ajax helpers here seems an unnecessary overhead and I suggest you just use jquery methods to update the DOM. Your current code suggests you might be missing some logic to make a comment voting system work, including indicating what action the user may have already performed. For example (and assuming you want it to work similar to SO), if a user has previously up-voted, then clicking on the up-vote link should decrement the vote count by 1, but clicking on the down-vote link should decrement the vote count by 2 (the previous up-vote plus the new down-vote).
Refer to this fiddle for how this might be styled and behave when clicking the vote elements
Your view model for a comment might look like
public enum Vote { "None", "Up", "Down" }
public class CommentVM
{
public int ID { get; set; }
public string Text { get; set; }
public Vote CurrentVote { get; set; }
public int TotalVotes { get; set; }
}
and assuming you have a model that contains a collection of comments
public class PostVM
{
public int ID { get; set; }
public string Text { get; set; }
public IEnumerable<CommentVM> Comments { get; set; }
}
and the associated DisplayTemplate
/Views/Shared/DisplayTemplates/CommentVM.cshtml
#model CommentVM
<div class="comment" data-id="#Model.ID" data-currentvote="#Model.CurrentVote">
<div class="vote">
<div class="voteup" class="#(Model.CurrentVote == Vote.Up ? "current" : null)"></div>
<div class="votecount">#Model.TotalVotes</div>
<div class="votedown" class="#(Model.CurrentVote == Vote.Down ? "current" : null)"></div>
</div>
<div class="commenttext">#Html.DisplayFor(m => m.Text)</div>
</div>
Then in the main view
#model PostVM
.... // display some properties of Post?
#Html.DisplayFor(m => m.Comments)
<script>
var voteUpUrl = '#Url.Action("VoteUp")';
var voteDownUrl = '#Url.Action("VoteDown")';
$('.voteup').click(function() {
var container = $(this).closest('.comment');
var id = container.data('id');
var voteCount = new Number(container.find('.votecount').text());
$.post(voteUpUrl, { id: id }, function(response) {
if (!response) {
// oops, something went wrong - display error message?
return;
}
container.find('.votecount').text(response.voteCount); // update vote count
if (response.voteCount < voteCount) {
// the user previously upvoted and has now removed it
container.find('.voteup').removeClass('current');
} else if (response.voteCount == voteCount + 1) {
// the user had not previously voted on this comment
container.find('.voteup').addClass('current');
} else if (response.voteCount == voteCount + 2) {
// the user previoulsy down voted
container.find('.votedown').removeClass('current');
container.find('.voteup').addClass('current');
}
});
});
$('.votedown').click(function() {
... // similar to above (modify logic in if/elseif blocks)
});
</script>
and the controller method
public JsonResult VoteUp(int id)
{
int voteCount = // your logic to calculate the new total based on the users current vote (if any) for the comment
return Json(new { voteCount = voteCount });
}
I am trying to use knockout with MVC strongly typed view. Since my model will have over 20 properties, I prefer to use strongly-typed view model to post back data by using ko.mapping.toJS and ko.Util.postJson. The Eligible field was passed back correctly, however the following code does not post back the selected option from drop down list, it just showed value as 0 when I looked that selectOptionModel on the controller. Can someone point out what I did wrong?
the view model from server side is as follows:
public class SelectOptionModel
{
public bool Eligible { get; set; }
public int selectedOption { get; set; }
public IEnumerable<SelectListItem> AvailableOptions
{
get
{
return Enum.GetValues(typeof(OptionEnum)).Cast<OptionEnum>()
.Select(x => new SelectListItem
{
Text = x.ToString(),
Value = x.ToString()
});
}
}
}
public enum OptionEnum
{
[Description("First")]
FirstOption = 1,
[Description("Second")]
SecondOption = 2,
[Description("Third")]
ThirdOption = 3
}
The razor view is like following:
#model TestKo.Models.SelectOptionModel
...
subViewModel = ko.mapping.fromJS(#Html.Raw(Json.Encode(Model)));
...
}
#using (Html.BeginForm()){
<button type="submit" class="button" id="SaveBtn">Save</button>
<div data-bind="with:vm">
<div>
#Html.LabelFor(model => model.Eligible)
#Html.CheckBoxFor(model => model.Eligible, new { data_bind = "checked: selectOptionVM.Eligible" })
</div>
<div>
#Html.LabelFor(model => model.selectedOption)
#Html.DropDownListFor(model => model.selectedOption, Model.AvailableOptions,
new
{ data_bind = "options: selectOptionVM.AvailableOptions, optionsText: 'Text', optionsValue: 'Value', value: selectOptionVM.selectedOption"
})
</div>
</div>
}
The javascript for the knockout view model is:
sectionVM = function (data) {
var self = this;
var selectOptionVM = data;
return {
selectOptionVM: selectOptionVM
}
}
$(document).ready(function () {
var viewModel = {
vm: new sectionVM(subViewModel)
};
ko.applyBindings(viewModel);
$("#SaveBtn").click(function () {
var optionModel = ko.toJS(viewModel.vm.selectOptionVM);
ko.utils.postJson($("form")[0], optionModel)
});
});
The controller part:
[HttpPost]
public ActionResult Create(SelectOptionModel selectOptionModel)
{
try
{
// TODO: Add insert logic here
var modelSaved = selectOptionModel;
return RedirectToAction("Index");
}
catch
{
return View();
}
}
I'm venturing a bit of a guess here, but this could be the problem: the id-bit of your selected option will always be a string (because it will go in the <option value="" attribute). Your endpoint expects an int. As far as I can see, you don't convert the selectedOption before sending it to the server. try parseInt(selectedOption, 10) before sending it to the server. Also, use the network tool in your browser to debug the info that is being sent to the controller. That might help you to zone in on the problem.
Actually it works. Somehow it was not working previously, but after I cleared cache, cookies etc, it just worked. Thanks everyone!
I have seen several examples on creating a HTML helper method for active menu items.
**Summary:** Simply put, in an MVC project, using the Twitter Bootstrap, I am trying to preserve the open state of a collapsible menu when a child is selected.
I am using a collapsible menu, where the parent's css (the selected item) needs to include active open if a child is selected. This will ensure that the menu is open at the right location. With the use of another HTML helper, the active item is already set to active.
HTML for the menu:
<div id="sidebar">
<ul>
<li class="active"><i class="icon-home"></i> <span>Dashboard</span></li>
<li class="submenu">
<i class="icon-beaker"></i> <span>UI Lab</span> <i class="arrow icon-chevron-right"></i>
<ul>
<li>Interface Elements</li>
<li>jQuery UI</li>
<li>Buttons & icons</li>
</ul>
</li>
<li class="submenu">
<i class="icon-th-list"></i> <span>Form elements</span> <i class="arrow icon-chevron-right"></i>
<ul>
<li>Common elements</li>
<li>Validation</li>
<li>Wizard</li>
</ul>
</li>
<li><i class="icon-th"></i> <span>Tables</span></li>
<li><i class="icon-th-list"></i> <span>Grid Layout</span></li>
<li class="submenu">
<i class="icon-file"></i> <span>Sample pages</span> <i class="arrow icon-chevron-right"></i>
<ul>
<li>Invoice</li>
<li>Support chat</li>
<li>Calendar</li>
<li>Gallery</li>
<li>Messages</li>
</ul>
</li>
<li>
<i class="icon-signal"></i> <span>Charts & graphs</span>
</li>
<li>
<i class="icon-inbox"></i> <span>Widgets</span>
</li>
</ul>
</div>
Here is the helper method for items:
public static MvcHtmlString MenuItem(this HtmlHelper htmlHelper,
string text,
string action,
string controller,
string iconClass)
{
var li = new TagBuilder("li");
var routeData = htmlHelper.ViewContext.RouteData;
var currentAction = routeData.GetRequiredString("action");
var currentController = routeData.GetRequiredString("controller");
if (string.Equals(currentAction, action, StringComparison.OrdinalIgnoreCase) &&
string.Equals(currentController, controller, StringComparison.OrdinalIgnoreCase))
{
li.AddCssClass("active");
}
var i = new TagBuilder("i");
i.AddCssClass(iconClass);
var basePath = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);
//li.InnerHtml = htmlHelper.ActionLink("<i>something</i>" + text, action, controller).ToHtmlString();
li.InnerHtml = htmlHelper.Raw(string.Format("<i class=\"{4}\"></i>{3}", basePath, controller, action, text, iconClass)).ToString();
return MvcHtmlString.Create(li.ToString());
}
And implemented like this:
<div id="sidebar">
<ul>
#Html.MenuItem("Dashboard", "Index", "Dashboard", "icon-home")
#* <li class="active"><i class="icon-home"></i> <span>Dashboard</span></li>*#
<li class="submenu">
<i class="icon-beaker"></i> <span>UI Lab</span> <i class="arrow icon-chevron-right"></i>
<ul>
<li>#Html.MenuItem("Websites", "Index", "Websites", null)</li>
<li>jQuery UI</li>
<li>Buttons & icons</li>
</ul>
</li>
<li class="submenu">
<i class="icon-th-list"></i> <span>Form elements</span> <i class="arrow icon-chevron-right"></i>
<ul>
<li>Common elements</li>
<li>Validation</li>
<li>Wizard</li>
</ul>
</li>
So what I don't have is something for the submenu items.
Is there a simpler way of trying to accomplish this?
--UPDATE--
I'm guessing that putting this into a partial view may be best. I need to find some way to preserve the selected item on click to reference it on every menu item, rather than check if the controller/action matches in order to set the current item to "active". A controller method that activates on click, checks if the currently selected item is a parent or child, and if the parent matches a child, then format differently? I'm sure there has to be an easier way.
Alright, so here is one solution I came up with.
To recap, this isn't as simple as adding an "active" CSS class to an item if it is selected (as per the default Bootstrap MVC. In this solution we need to identify the parent of and a child and identify both.
Default page is Dashboard. The user then clicks on "Configuration" to expand the menu, then selects the "Websites" link which opens the page.
Here is the solution:
Model:
public class NavigationMenu
{
public string Text { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public string Icon { get; set; }
public bool Selected { get; set; }
public List<NavigationMenu> MenuChildren;
}
Controller:
public class NavigationController : Controller
{
[ChildActionOnly]
public ActionResult GenerateMenu()
{
List<NavigationMenu> menuItems = new List<NavigationMenu>();
// build the menu
menuItems.Add(new NavigationMenu
{
Text = "Dashboard",
Action = "",
Controller = "Dashboard",
Icon = "icon-home",
Selected = true, // default selected menu item
MenuChildren = null
});
menuItems.Add(new NavigationMenu
{
Text = "Configuration",
Action = null,
Controller = null,
Icon = "icon-cog",
MenuChildren = new List<NavigationMenu>{
new NavigationMenu{
Text = "Websites",
Action = "",
Controller = "Websites",
Icon = null,
MenuChildren = null
},
new NavigationMenu{
Text = "Child 2",
Action = null,
Controller = null,
Icon = null,
MenuChildren = null
}
}
});
menuItems.Add(new NavigationMenu
{
Text = "Item 2",
Action = "",
Controller = "Item2",
Icon = "icon-random",
MenuChildren = null
});
menuItems.Add(new NavigationMenu
{
Text = "Item 3",
Action = "",
Controller = "Item3",
Icon = "icon-certificate",
MenuChildren = null
});
string action = ControllerContext.ParentActionViewContext.RouteData.Values["action"].ToString() == "Index" ? "" : ControllerContext.ParentActionViewContext.RouteData.Values["action"].ToString();
string controller = ControllerContext.ParentActionViewContext.RouteData.Values["controller"].ToString();
foreach (var item in menuItems)
{
if (item.MenuChildren != null)
{
foreach (var cItem in item.MenuChildren)
{
if (cItem.Controller == controller && cItem.Action == action)
{
cItem.Selected = true;
break;
}
else
{
cItem.Selected = false;
}
}
}
if (item.Controller == controller && item.Action == action)
{
item.Selected = true;
break;
}
else
{
item.Selected = false;
}
}
return PartialView("~/Views/Shared/_Navigation.cshtml", menuItems);
}
}
Shared View:
#model IEnumerable<AdminWebsite.Models.NavigationMenu>
#{
var basePath = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);
}
<div id="sidebar">
#Html.Raw("<ul>")
#foreach (var item in Model)
{
// if the menu item does not have children then it should be clickable
if (item.MenuChildren == null & item.Selected)
{
<li class="active"><i class="#item.Icon"></i> <span>#item.Text</span></li>
}
else if (item.MenuChildren == null & !item.Selected)
{
<li><i class="#item.Icon"></i> <span>#item.Text</span></li>
}
// has children and one of its children is selected
if (item.MenuChildren != null)
{
if (item.MenuChildren.Any(c => c.Selected) == true)
{
<text><li class="submenu active open"></text>
}
else
{
<text><li class="submenu"></text>
}
// sub-menu parent
if (item.MenuChildren != null & item.Selected)
{
<i class="#item.Icon"></i> <span>#item.Text</span>
}
else if (item.MenuChildren != null & !item.Selected)
{
<i class="#item.Icon"></i> <span>#item.Text</span>
}
// children
<text><ul></text>
// iterate through children
foreach(var cItem in item.MenuChildren)
{
if (cItem.MenuChildren == null & cItem.Selected)
{
<li class="active"><i class="#cItem.Icon"></i> <span>#cItem.Text</span></li>
}
else if (cItem.MenuChildren == null & !cItem.Selected)
{
<li><i class="#cItem.Icon"></i> <span>#cItem.Text</span></li>
}
}
#Html.Raw("</ul>");
#Html.Raw("</li>");
}
}
#Html.Raw("</ul>")
</div>
Implementation in the view:
#{Html.RenderAction("GenerateMenu", "Navigation");}
The controller checks if the current action/controller names match one on the menu and if so, set selected = true. In the partial view, there is some logic to determine the display structure, based on the parent/child relationships, and if a child is selected, so is the parent.
In brief, that's it. I'd like to hear some comments/other examples.
Here is a solution using most of the code from the accepted answer, refactored to use HtmlHelpers and TagBuilders with a little renaming to fit my project.
Model:
public class MenuViewModel
{
public IList<MenuItemDto> MenuItems;
}
public class MenuItemDto
{
public string Text { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public string IconCssClass { get; set; }
public bool Active { get; set; }
public List<MenuItemDto> MenuChildren;
}
Controller:
public ActionResult GenerateMenu()
{
var viewModel = new MenuViewModel();
viewModel.MenuItems = //code to build menu model like ElHaix provided in his Controller;
return PartialView("~/Views/Shared/_Menu.cshtml", viewModel);
}
Shared View:
#using Extensions
<div id="sidebar">
#Html.Raw("<ul>")
#foreach (var item in Model.MenuItems)
{
// if the menu item does not have children then it should be clickable
if (item.MenuChildren == null)
{
#Html.LiForMenuItem(item)
}
// has children and one of its children is selected
if (item.MenuChildren != null)
{
if (item.MenuChildren.Any(c => c. Active) == true)
{
<text><li class="submenu active open">
</text>
}
else
{
<text>
<li class="submenu">
</text>
}
// sub-menu parent
if (item.MenuChildren != null)
{
#Html.HrefForSubMenuItemRoot( item)
}
// children
<text><ul>
</text>
// iterate through children
foreach (var cItem in item. MenuChildren)
{
if (cItem.MenuChildren == null)
{
#Html.LiForMenuItem(cItem)
}
}
#Html.Raw("</ul>");
#Html.Raw("</li>");
}
}
#Html.Raw("</ul>")
</div>
Html Helpers:
namespace Extensions
{
public static class MenuExtensions
{
public static MvcHtmlString LiForMenuItem(this HtmlHelper htmlHelper, MenuItemDto menuItem)
{
var li = new TagBuilder("li");
AddActiveCssClassToTag(menuItem, li);
var contentUrl = GenerateContentUrlFromHttpContext(htmlHelper);
li.InnerHtml = GenerateLinkForMenuItem(menuItem, contentUrl);
return MvcHtmlString.Create(li.ToString());
}
public static MvcHtmlString HrefForSubMenuItemRoot(this HtmlHelper htmlHelper, MenuItemDto menuItem)
{
var a = new TagBuilder("a");
AddActiveCssClassToTag(menuItem, a);
var contentUrl = GenerateContentUrlFromHttpContext(htmlHelper);
a.Attributes.Add("href", GenerateUrlForMenuItem(menuItem, contentUrl));
a.InnerHtml = GenerateInnerHtmlForMenuItem(menuItem);
return MvcHtmlString.Create(a.ToString());
}
private static void AddActiveCssClassToTag(MenuItemDto menuItem, TagBuilder tag)
{
if (menuItem.Active)
{
tag.AddCssClass("active");
}
}
private static string GenerateContentUrlFromHttpContext(HtmlHelper htmlHelper)
{
return UrlHelper.GenerateContentUrl("~/", htmlHelper.ViewContext.HttpContext);
}
private static string GenerateLinkForMenuItem(MenuItemDto menuItem, string contentUrl)
{
var a = new TagBuilder("a");
a.Attributes.Add("href", GenerateUrlForMenuItem(menuItem, contentUrl));
a.InnerHtml = GenerateInnerHtmlForMenuItem(menuItem);
return a.ToString();
}
private static string GenerateInnerHtmlForMenuItem(MenuItemDto menuItem)
{
var html = string.Empty;
//Add <i></i> if there is an IconCssClass present
var i = new TagBuilder("i");
if (!String.IsNullOrEmpty(menuItem.IconCssClass))
{
i.AddCssClass(menuItem.IconCssClass);
html += i.ToString();
}
//add a span for the text of the menuItem
var span = new TagBuilder("span");
span.InnerHtml = menuItem.Text;
html += span.ToString();
return html;
}
private static string GenerateUrlForMenuItem(MenuItemDto menuItem, string contentUrl)
{
var url = contentUrl + menuItem.Controller;
if (!String.IsNullOrEmpty(menuItem.Action)) url += "/" + menuItem.Action;
return url;
}
}
}