Therefore I faced an issue with MVC ActionLink and bootstrap dropdown. I succeeded with simple menu extension where I passed such a parameters like strings and one bool. But now I am trying to make my own extension which could generate Bootstrap Dropdown and add selected css class to parent of the dropdown - "ONEofTHEdropdownITEMSselected" - when one of those items in dropdown is selected (when selecting dropdown item it routes to different controller there fore can be few or more controllers):
Dropdown <b class="caret"></b>
and
<li class="dropdown">
Dropdown <b class="caret"></b>
<ul class="dropdown-menu">
<li>Action1</li>
<li>Action2</li>
</ul>
</li>
Below is my UI/MenuExtensions.cs what I am trying to achieve - to pass two parameters which could generate the bootstrap dropdown and I can manually insert new menu items in that dropdown.
public static class MenuExtensions
{
public static MvcHtmlString MenuItem(
this HtmlHelper htmlHelper,
string text,
string action,
string controller,
string cssClass = "item",
bool isController = false
)
{
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) || isController) &&
string.Equals(currentController, controller, StringComparison.OrdinalIgnoreCase))
li.AddCssClass("am-selected");
li.InnerHtml = htmlHelper.ActionLink(text, action, controller, new { Area = "" }, new { #class = cssClass }).ToHtmlString();
return MvcHtmlString.Create(li.ToString());
}
public static MvcHtmlString SelectMenu(
this HtmlHelper htmlHelper,
string cssClass,
SelectMenuItem[] menuItems
)
{
TagBuilder list = new TagBuilder("li")
{
InnerHtml = ""
};
string currentAction = htmlHelper.ViewContext.RouteData.GetRequiredString("action");
string currentController = htmlHelper.ViewContext.RouteData.GetRequiredString("controller");
foreach (SelectMenuItem menuItem in menuItems)
{
TagBuilder li = new TagBuilder("li")
{
InnerHtml = htmlHelper.ActionLink(menuItem.Text, menuItem.Action, menuItem.Controller, null, new { }).ToHtmlString()
};
ul.InnerHtml += li.ToString();
}
return MvcHtmlString.Create(list.ToString());
}
}
Here is the external class
public class SelectMenuItem
{
public string Text { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public bool IsVisible { get; set; }
public SelectMenuItem()
{
IsVisible = true;
}
}
After that my html looks like this.
#Html.SelectMenu("dropdown", new []{
new SelectMenuItem{ Text = "ViewOne", Controller = "Controller1", Action = "index", IsVisible = SystemUser.Current().IsAdmin},
new SelectMenuItem{ Text = "ViewTwo", Controller = "Controller2", Action = "index"}
});
The problem is SelectMenu renders only this:
<li></li>
No need to reinvent the wheel. With TwitterBootstrapMVC desired output is achieved with the following syntax:
#using (var dd = Html.Bootstrap().Begin(new DropDown("Dropdown").SetLinksActiveByControllerAndAction()))
{
#dd.ActionLink("Action1", "index", "controller1")
#dd.ActionLink("Action2", "index", "controller2")
}
Notice the extension method SetLinksActiveByControllerAndAction(). That's what makes links active based on current controller/action.
Disclaimer: I'm the author of TwitterBootstrapMVC.
You need to purchase a license if working with Bootstrap 3. For Bootstrap 2 it's free.
Related
I'm working in an ASP.net MVC application, and I have a table of products as shown in the screenshot:
I would like the ability to filter that table of products, and I'd like the filtering to happen via the query string params (as a GET) so that the URL can be shared.
The ViewModel for the page is like this:
public class InventoryReportViewModel
{
public SearchViewModel Search { get; set; } // 2 string props [Type and Term]
public IEnumerable<ProductViewModel> Products { get; set; }
public PaginationViewModel Pagination { get; set; } // 3 int props [currentPage, recordsPerPage, totalRecords]
}
I'm using Razor helpers to draw the filter inputs, like this:
#Html.EditorFor(m => m.Search.Term, new { htmlAttributes = new { #class = "form-control" } })
And also I've set up my form to use GET like so:
#using (Html.BeginForm("Inventory", "Report", FormMethod.Get))
{
// form elements
}
My ReportController.cs has the following method that is relevant to my question here:
public ActionResult Inventory(string SearchTerm, string SearchType, int page = 1)
{
var viewModel = _reportService.GetProducts(page, SearchTerm, SearchType);
return View(viewModel);
}
When I pass a Search term, and click the Filter Results button, I do arrive at my Controller method above, but the SearchTerm and SearchType are null.
I know how to "hack" this to work, for example, if I do this:
<input type="text" name="SearchTerm" class="form-control"/>
Then the search term I input would be picked up by the Controller, but is there no other way?
since you already made a viewmodel for Search
public SearchViewModel Search { get; set; }
you just need to pass it to the controller like this
public ActionResult Inventory(SearchViewModel Search, int page = 1
{
var viewModel = _reportService.GetProducts(page, Search.Term, Search.Type);
return View(viewModel);
}
you were getting null because the textboxes were named as Search.Term that is why it was not matching the parameters.
The form should be post
#using (Html.BeginForm("Inventory", "Report", FormMethod.Post))
{
// form elements
}
This can also be cleaner:
#Html.EditorFor(m => m.Search.Term, new { htmlAttributes = new { #class = "form-control" } })
to
#Html.EditorFor(m => m.Search.Term, new { #class = "form-control" } )
Another question,
In the razor view, do you have a model specified on the first line?
i have implemented one web grid with pagination enabled using webgridextension.cs file. Same page have multiple buttons to perform different actions like search, get excel, get pdf. in order to perform different form submits i used multiplebutton with action selector attribute. when i click on page number down to the grid it was navigating to default action index.
Here i want to navigate to different action GetRFQData with model (already loaded) in the page..
can you please help me on this.
WebGrid grid = new WebGrid(null, rowsPerPage: 25, canPage: true, defaultSort: "RFQID");
grid.Bind(Model != null ? Model.RFQSearchResults != null ? Model.RFQSearchResults
: new List<Shipsurance.Model.RFQ>()
: new List<Shipsurance.Model.RFQ>(), rowCount: Model != null ? Model.TotalRowsCount :25, autoSortAndPage: false);
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MultipleButtonAttribute : ActionNameSelectorAttribute
{
public string Name { get; set; }
public string Argument { get; set; }
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
var isValidName = false;
var keyValue = string.Format("{0}:{1}", Name, Argument);
var value = controllerContext.Controller.ValueProvider.GetValue(keyValue);
if (value != null)
{
controllerContext.Controller.ControllerContext.RouteData.Values[Name] = Argument;
isValidName = true;
}
return isValidName;
}
}
[MultipleButton(Name = "action", Argument = "GetRFQData")]
public ActionResult GetData(Shipsurance.Model.RFQ rfqModelData)
{
SerachRFQ getRFQDetails = new SerachRFQ();
return View("Index", getDetails.getResults(rfqModelData));
}
You can use RedirectToAction() method.
return RedirectToAction("Your action", model);
I am using MVC5, Razor, Entity Framework, C#. I am trying to pass a value of a dorpdown list using a link.
my model is
public class TestVM
{
public string TheID { get; set; }
}
I am loading an enum into a IEnumerable<SelectListItem>.
My enum is
public enum DiscountENUM
{
SaleCustomer,
SaleCustomerCategory,
SaleProduct,
SaleProductCategory,
SaleCustomerAndProduct,
SaleCustomerAndProductCategory,
SaleCustomerCategoryAndProductCategory,
PurchaseVendor,
PurchaseVendorAndProduct,
PurchaseVendorAndProductCategory,
PurchaseProduct,
PurchaseProductCategory,
Unknown
}
I am using the index method of the home controller
public ActionResult Index()
{
ViewBag.ListOfDiscounts = SelectListDiscountENUM();
TestVM d = new TestVM();
return View(d);
}
Where I load the ListOfDiscounts using:
private IEnumerable<SelectListItem> SelectListDiscountENUM()
{
List<SelectListItem> selectList = new List<SelectListItem>();
var listOfEnumValues = Enum.GetValues(typeof(DiscountENUM));
if (listOfEnumValues != null)
if (listOfEnumValues.Length > 0)
{
foreach (var item in listOfEnumValues)
{
SelectListItem sVM = new SelectListItem();
sVM.Value = item.ToString();
sVM.Text = Enum.GetName(typeof(DiscountENUM), item).ToString();
selectList.Add(sVM);
}
}
return selectList.OrderBy(x => x.Text).AsEnumerable();
}
My create method which is called from the view is
public ActionResult Create(TestVM d, string TheID)
{
return View();
}
My Index view is
#model ModelsClassLibrary.Models.DiscountNS.TestVM
<div>#Html.ActionLink("Create New", "Create", new { TheID = Model.TheID})</div>
<div>
#Html.DropDownListFor(x => x.TheID, #ViewBag.ListOfDiscounts as IEnumerable<SelectListItem>, "--- Select Discount Type ---", new { #class = "form-control" })
</div>
The problem is in the following line in the View
<div>#Html.ActionLink("Create New", "Create", new { TheID = Model.TheID })</div>
I have tried adding a model with the name of the field as "TheID"... no luck. Also, added a string field in the parameter, no luck. I looked at the FormControl object, and there was nothing in it either! I suspect something has to be added at the Route level in the helper, but I don't know what.
Model.TheID is always null. Even when I select an item in the DropDownListFor.
Does anyone have an idea how I can capture the select value of the DropDownListFor and send it into the Html.ActionLink TheID?
I am trying to return the current dynamic View to allow me to append a css class to an ActionLink if the current View is the same as the ActionLink.
As I am passing the majority of links through a specific route, in this case Pages, the currentAction will always be Pages in most cases, despite the actual View or Template being returned from the ActionResult called.
So for example if the url is http://mytestdomain.com/sport I would like the currentAction to be Sport and not Pages.
Please see my code below:
RouteConfig.cs
routes.MapRoute("Pages", "{mainCategory}/{subCategory}/{pageName}", new { controller = "Home", action = "Pages", subCategory = UrlParameter.Optional, pageName = UrlParameter.Optional });
HomeController
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName)
{
var currentController = htmlHelper.ViewContext.ParentActionViewContext.RouteData.GetRequiredString("controller");
var currentAction = htmlHelper.ViewContext.ParentActionViewContext.RouteData.GetRequiredString("action");
var currentView = htmlHelper.CurrentViewName();
var builder = new TagBuilder("li")
{
InnerHtml = htmlHelper.ActionLink(linkText, actionName, controllerName).ToHtmlString()
};
builder.AddCssClass("dropdown");
var actionSplit = actionName.TrimStart('/').Split('/');
actionName = actionSplit[0];
if (controllerName == currentController && actionName == currentAction)
{
return new MvcHtmlString(builder.ToString().Replace("a href", "a class=\"active\" href").Replace("</li>", "").Replace("Home/", ""));
}
return new MvcHtmlString(builder.ToString().Replace("</li>", "").Replace("Home/", ""));
}
public static string CurrentViewName(this HtmlHelper html)
{
return System.IO.Path.GetFileNameWithoutExtension(((RazorView)html.ViewContext.View).ViewPath);
}
public ActionResult Pages(string mainCategory, string subCategory, string pageName)
{
if (!string.IsNullOrEmpty(pageName))
{
subCategory = subCategory + "/" + pageName;
}
Page model;
using (CMSEntities)
{
model = (from f in CMSEntities.GetPage(1, mainCategory, subCategory, "Live") select f).FirstOrDefault();
}
return View(model.Template, model);
}
Navigation.cshtml
#Html.MenuLink(navigation.Title, "/" + Html.ToFriendlyUrl(navigation.Title), "Home")
I have tried using var currentView = htmlHelper.CurrentViewName(); but this will always return Navigation as the ActionLink is being called from within a [ChildActionOnly] public ActionResult Navigation() for example #{ Html.RenderAction("Navigation", "Home"); } from within Views/Shared/_Layout.cshtml
Any help would be much appreciated :-)
In the end I used 'HttpContext.Current.Request.Url.AbsolutePath' to determine the current location to append the active class to the matching page link.
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName)
{
var currentController = htmlHelper.ViewContext.ParentActionViewContext.RouteData.GetRequiredString("controller");
var currentUrl = HttpContext.Current.Request.Url.AbsolutePath.TrimStart('/').Split('/');
var mainCategory = currentUrl[0];
var builder = new TagBuilder("li")
{
InnerHtml = htmlHelper.ActionLink(linkText, actionName, controllerName).ToHtmlString()
};
builder.AddCssClass("dropdown");
var actionSplit = actionName.TrimStart('/').Split('/');
actionName = actionSplit[0];
if (actionSplit.Length == 1)
{
if (controllerName == currentController && actionName == mainCategory)
{
return new MvcHtmlString(builder.ToString().Replace("a href", "a class=\"active\" href").Replace("</li>", "").Replace("Home/", ""));
}
}
return new MvcHtmlString(builder.ToString().Replace("</li>", "").Replace("Home/", ""));
}
I hope this proves useful to others :-)
So I have this application in ASP MVC 3.
My database has two tables: Comenzi and DetaliiComenzi with one-to-many relationship (and Link-to-Sql) - in my application I want my users to buy some products by making a oder(stored in table comenzi) and for that order a list of products he wants to buy (will be stored in DetaliiComenzi with Order.id as foreign key).
Basically, after I create a new entry for Comenzi, I want to be able to make a list of products for that order (something like a shop chart but the user will choose his products in a view, adding how many products as he likes).
I have used Steve Sanderson’s method of editing (and creating) a variable length list.
-- Here is the model for which I create the list.
When I'm choosing a single product to oder I must first select the Group (Grupa) which he belongs to from a dropdownlist (using ListaGrupe) and then from a second dropdownlist (ListaProduse) a product from that particular group of products I selected in the first dropdownlist.
public class Comd
{
public string Grupa { get; set; }
public string Produs { get; set; }
public string Cantitate { get; set; }
public string Pret { get; set;}
public string TVA { get; set; }
public string Total { get; set; }
public List<SelectListItem> ListaGrupe
{
get;
set;
}
public List<SelectListItem> ListaProduse
{
get;
set;
}
}
--The Controller:
public ActionResult ComandaDetaliu(string id)
{
Comd model = new Comd();
IProduseRepository _prod = new ProduseRepository();
model.ListaGrupe = _listecomanda.GetGrupe();
string first = model.ListaGrupe[0].Value;
model.ListaProduse = _listecomanda.GetProduse(first);
string pret = _prod.GetByDenumire(model.ListaProduse[0].Text).pret.ToString();
model.Pret = pret;
double fr = 0.24;
model.TVA = fr.ToString();
var data = new[] { model };
return View(data);
}
-- The View
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<InMVC3.Models.Comd>>" %>
<%# Import Namespace="InMVC3.Helpers"%>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Comanda numarul: <%: RouteData.Values["id"].ToString()%></h2>
<% using(Html.BeginForm()) { %>
<div id="editorRows">
<% foreach (var item in Model)
Html.RenderPartial("ProduseEditor", item);
%>
</div>
<%= Html.ActionLink("Adauga alt produs", "Add", null, new { id = "addItem" }) %>
<input type="submit" value="Finished" />
<% } %>
-- The Partial View "Produse Editor"
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<InMVC3.Models.Comd>" %>
<%# Import Namespace="InMVC3.Helpers" %>
<div class="editorRow">
<script type="text/javascript">
$(document).ready(function () {
$("#Grupa").change(function () {
var url = '<%= Url.Content("~/") %>' + "Comenzi/ForProduse";
var ddlsource = "#Grupa";
var ddltarget = "#Produs";
$.getJSON(url, { id: $(ddlsource).val() }, function (data) {
$(ddltarget).empty();
$.each(data, function (index, optionData) {
$(ddltarget).append("<option value='" + optionData.Value + "'>" + optionData.Text + "</option>");
});
});
});
});
</script>
<% using(Html.BeginCollectionItem("comds")) { %>
Grupa: <%= Html.DropDownListFor(x => x.Grupa, Model.ListaGrupe) %>
Produsul: <%= Html.DropDownListFor(x => x.Produs, Model.ListaProduse) %>
Cantitate: <%=Html.TextBoxFor(x=>x.Cantitate) %>
Pret: <%=Html.DisplayFor(x => x.Pret, new { size = 4})%>
TVA: <%= Html.DisplayFor(x=>x.TVA) %>
Total: <%=Html.DisplayFor(x=>x.Total) %>
Sterge
<% } %>
-- And the JsonResult method
public JsonResult ForProduse(string id)
{
throw new NotSupportedException();
JsonResult result = new JsonResult();
var produsele = _listecomanda.GetProduse(id);
result.Data = produsele;
result.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
return result;
}
All I need to know is how to make the call to the JsonResult action because this is what doesn't works so that when I change the selected value in the first dropdownlist to dynamically change the second too.
Of course, I also need to change the other properties too but that after I get how to make getJson to work.
If you need more details please tell me.
UPDATE 1:
--The Helper
public static class HtmlPrefixScopeExtensions
{
private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}
public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
{
return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
}
private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
{
// We need to use the same sequence of IDs following a server-side validation failure,
// otherwise the framework won't render the validation error messages next to each item.
string key = idsToReuseKey + collectionName;
var queue = (Queue<string>)httpContext.Items[key];
if (queue == null) {
httpContext.Items[key] = queue = new Queue<string>();
var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
if (!string.IsNullOrEmpty(previouslyUsedIds))
foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
queue.Enqueue(previouslyUsedId);
}
return queue;
}
private class HtmlFieldPrefixScope : IDisposable
{
private readonly TemplateInfo templateInfo;
private readonly string previousHtmlFieldPrefix;
public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
{
this.templateInfo = templateInfo;
previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
}
public void Dispose()
{
templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
}
}
}
UPDATE
I now have another issue. When I Post that list to the actiont, I get the following error at the foreach statement inside the controller action: Object reference not set to an instance of an object.
-- The Controller Action
[HttpPost]
public ActionResult ComandaDetaliu(IEnumerable<Comd> comenzi)
{
if (ModelState.IsValid)
{
foreach (var item in comenzi)
{
detalii_comenzi det = new detalii_comenzi();
det.id_comanda = Convert.ToInt32(RouteData.Values["id"].ToString());
det.id_produs = Convert.ToInt32(item.Produs);
det.cantitate_comandata = Convert.ToDecimal(item.Cantitate);
det.cantitate_livrata = 0;
det.pret =Convert.ToDecimal(item.Pret);
det.tvap = Convert.ToDecimal(item.TVA);
}
return RedirectToAction("Index");
}
return View(comenzi);
}
Your problem is the duplicate IDs - Every row has a dropdown with ID "Grupa" so your jquery selector will match the dropdowns in every row.
You need to add a prefix to the controls - there are several ways to achieve that - a search for "mvc3 field prefix" brings up several other questions:
How to define form field prefix in ASP.NET MVC
ASP.MVC 3 Razor Add Model Prefix in the Html.PartialView extension
ASP.NET MVC partial views: input name prefixes
Most of those are focused on mapping when the form is posted, but the same issue applies with your javascript.
You could just update the ids in your script to something like "##(ViewBag.Prefix)Grupa", but a better approach would be to use classes instead of ids in your selector and make the script reusable - something like:
ddlSource = this;
ddlDest = this.Parent().find(".produs");