MVC Ajax with Dynamic Partial View Creation - asp.net-mvc-4

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 });
}

Related

Blazor. NavLink is not working in a foreach block when clicking multiply times

I created list of NavLink, this is only one difference between them, dynamic parameter Id
<div>
#foreach (var service in pageGlobal.Person.Services)
{
var link = $"service_description/{service.Identifier}";
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="#link">
#service.Name
</NavLink>
</li>
</ul>
}
</div>
And it works only if I click first time(no matter what NavLink item it works properly).
Click to the next link - url is changed but nothing happens.
Code of the ServiceDescription.razor
#page "/service_description/{Id:int}"
#using Site.Data
#using Site.ViewModels
#inject StartUpService page
#if (service != null)
{
<div> <h3>Service Name - #service.Name</h3> - <h4>#service.Description</h4> </div>
}
#code
{
[Parameter]
public int Id { get; set; }
PageGlobal pageGlobal;
Service service;
protected override async Task OnInitializedAsync()
{
pageGlobal = await page.GetPageGlobalAsync();
service = pageGlobal.Person.Services.Where(e => e.Identifier == Id).FirstOrDefault();
}
}
How can I force client reload ServiceDescription.razor with a new parameter using NavLink functionality?
This issue is caused by OnInitializedAsync. For OnInitializedAsync, it will be called only when the component is invoked when the component is ready to start.
If you want to change service based on parameter, you should use OnParametersSetAsync like below:
protected override async Task OnParametersSetAsync()
{
pageGlobal = new PageGlobal
{
Person = new Person()
{
Services = new List<Service>(){
new Service{ Identifier = 1, Name = "Test1", Description = "D1" },
new Service{ Identifier = 2, Name = "Test2" , Description = "D2" },
new Service{ Identifier = 3, Name = "Test3", Description = "D3" }
}
}
};
service = pageGlobal.Person.Services.Where(e => e.Identifier == Id).FirstOrDefault();
}
You could check ComponentBase

RadioButton list Binding in MVC4

I have a radiobuttonList which is binding data from Enum Class and its working correctly in the view.
But my concern is how can I set inital value of radiobutton to CROCount.ONE.I have tried to set the initial value in the following way but couldnot get the desired result.
public enum CROCount
{
ONE = 1,
TWO = 2
}
ViewModel is
public class RegistraionVM
{
....
public EnumClass.CROCount CROCount { get; set; }
}
I generated the radio button list as follows.
<div>
#foreach (var count in Enum.GetValues(typeof(SMS.Models.EnumClass.CROCount)))
{
<label style="width:75px">
#Html.RadioButtonFor(m => m.RegistrationVenue, (int)count,
new { #class = "minimal single" })
#count.ToString()
</label>
}
</div>
Binding performed in the Controller is
public ActionResult Index(int walkInnId)
{
try
{
var _studentReg = new RegistraionVM
{
CROCount=EnumClass.CROCount.ONE
};
return View(_studentReg);
}
catch (Exception ex)
{
return View("Error");
}
}
Your binding your radio button to property CROCount (not RegistrationVenue) so your code should be
#Html.RadioButtonFor(m => m.CROCount, count, new { id = "", #class = "minimal single" })
Note that the 2nd parameter is count (not (int)count) so that you generate value="ONE" and value="TWO". Note also the new { id = "", removes the id attribute which would otherwise result in duplicate id attributes which is invalid html.

How to save Dictionary object to database in MVC 4?

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)
{
}

using MVC4 Strongly typed view with Knockout

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!

Is there a simple MVC4 html helper for collapsible (multi-level) Bootstrap 3 menus?

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;
}
}
}