MVC routing issues, areas and HTML actionlinks - asp.net-mvc-3

I've looked for this for a while and the solutions look like they should work, it appears I'm doing it wrong.
I created an area for administering the CMS side called "manage" so if you go to:
/Manage/Vinyard
it will give you a list of vinyards to manage using VinyardController built with the CRUD scaffold.
On the "front end" I have a browse controller and another VinyardController for viewing the details of a vinyard.
So someone goes to
/Browse/Vinyard
it gives them a list of Vinyards, they click on one (here's the problem) I want it to go to
/Vinyard/NameOfVinyard
The route that I have is:
routes.MapRoute(
"Vinyard",
"Vinyard/{Name}",
new { controller = "VinyardController", action = "Details", area="root"}
);
Which is above the default route. Details is the method that displays the Vinyard details.
the HTML.actionlink I'm using is:
#Html.ActionLink(item.Name, "Details", "vinyard" ,new { name = item.VinyardId, area="root" })
for some reason the a tag that's returned is: /Browse/Details?Length=7
On top of that when I try to browse to /vinyard/1 it gives me a 404.
Thanks for your help!
Update: If I browse to /vinyard/details/1 it works properly, except that I want it to eschew the /details/ part.

Use this overload
public static MvcHtmlString ActionLink(
this HtmlHelper htmlHelper,
string linkText,
string actionName,
string controllerName,
Object routeValues,
Object htmlAttributes
)
So change your code to
#Html.ActionLink(item.Name, "Details", "vinyard" ,
new { name = item.VinyardId, area="root" },null)

Fixed it. Working with Shyju's modified action link, but I also removed the area property and had to fix up the map routing for it to work right.
My global.ascx now looks like this:
routes.MapRoute(
"Vinyard",
"Vinyard/{id}",
new { controller = "Vinyard", action = "Details", id=UrlParameter.Optional},
new[] { "MyNameSpace.Controllers" }
);
The two problems were: my controller name needed to be "Vinyard" not "VinyardController" and I needed to add the name space here and in my area route registration since I was using the same class names in both areas.

Related

Html.ActionLink equivalent of a specific URL format

I want to use Html.ActionLink to return an anchor tag with href like /Product/22/Product-Name, where Product is my controller name and 22 is my id and Product-Name is my title.
In my global.asax, I have set the following route :
routes.MapRoute(
"ProductRoute", // Route name
"Product/{id}/{title}", // URL with parameters
new { controller = "Product", action = "Index", id = UrlParameter.Optional });
I tried using following code
#Html.ActionLink(model.Name, null, "Product",
new { id = model.Id, title = model.Name.Replace(" ","-") }, null)
but it yields Product/Browse/22?title=Product-Name. This code is in Browse View returned by Browse action of Catalog controller and that's why it picks up the Browse in between.
Strangely, the same code works fine in the Index View returned by Index action of the Home Controller. What's the reason for this difference? . Am I missing something or this is a bug in Asp.net MVC3.
What Should I do to achieve my stated URL format. For the meantime, I am using this code
#product.Name
make sure you have defined the custom route before the default route and try
#Html.ActionLink(model.Name,
"Index",
new {controller="Product", id = model.Id, title = model.Name.Replace(" ","-") }, null)
the first parameter is the DisplayName, second the ActionResult's name and then the RouteValues

Adding HtmlAttributes to template

If I am passing HtmlAttributes into a template, like this:
#Html.DisplayFor(m => m.FirstName, new { htmlAttributes = new { #class = "orangetxt strongtxt" } })
In my template, how would I inject these into my HTML:
<span #ViewData["htmlAttributes"]>#Model</span>
This almost works, but it does some pretty weird stuff, so I'm assuming this isn't the way to go.
I realize I can accomplish this with an HtmlHelper extension method to render the full HTML element (span, in this case) and pass in the attributes that way, but is there a way to just render attributes straight into an HTML element, like the above example?
The below extension method will allow me to convert HtmlAttributes to a string:
public static MvcHtmlString RenderHtmlAttributes<TModel>(
this HtmlHelper<TModel> htmlHelper, object htmlAttributes)
{
var attrbituesDictionary = new RouteValueDictionary(htmlAttributes);
return MvcHtmlString.Create(String.Join(" ",
attrbituesDictionary.Select(
item => String.Format("{0}=\"{1}\"", item.Key,
htmlHelper.Encode(item.Value)))));
}
Then, to render them within the tag, I can just do this:
<span #Html.RenderHtmlAttributes(ViewData["htmlAttributes"])>#Model</span>
Jerad Rose's answer is good, but I ran into couple of issues with it:
It does not not convert underscores to dashes in attribute names
It does not handle no-value attributes gracefully
To address first issue, use HtmlHelper.AnonymousObjectToHtmlAttributes.
Below is my modification of Jerad's method:
public static MvcHtmlString RenderHtmlAttributes(this HtmlHelper helper, object htmlAttributes)
{
if (htmlAttributes == null) return new MvcHtmlString(String.Empty);
var attrbituesDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
return new MvcHtmlString(String.Join(" ", attrbituesDictionary.Select(item => string.IsNullOrEmpty((string)item.Value) ? String.Format("{0}", item.Key) : String.Format("{0}=\"{1}\"", item.Key, helper.Encode(item.Value)))));
}
Try this instead,
#Html.DisplayFor(m => m.FirstName,
new { htmlAttributes = "class = orangetxt strongtxt"})
This will render a string, whereas your version did do weird stuff, rendered { } as part of the output.
DisplayFor() is used to render the template that matches the property type.
Display templates are .cshtml files inside /DisplayTemplates folder which in turn is inside a view folder (i.e. any folder from Home, Shared or even a specific controller).
An example.
If you've a String.cshtml template like this inside /Views/Shared:
#model String
#if (string.IsNullOrEmpty(Model)) {
<span>(no string)</span>
}
else {
<span>#Model</span>
}
Every time you call DisplayFor() for a string property:
DisplayFor(model => model.MyStringProperty);
It renders the template accordingly to the string's value. You can be more specific and put /DisplayTemplates inside a specific View folder and them only calls from those views are affected by the template.
In your case you can be even more specific and call DisplayFor() with a particular template.
Suppose you've a template for a particular property, called MyPropertyTemplate.cshtml. You would call DisplayFor() like this:
DisplayFor(model => model.MyProperty, "MyPropertyTemplate");
And them, inside that template you can have whatever HTML attributes you want.
#model MyProperty
<span class="orangetxt strongtxt">#MyProperty.ToString()</span>
PS: When it doesn't find a template I guess it only calls model.Property.ToString() without additional html.
FYI: EditorFor(), for example, works in a similar way but it uses /EditorTemplates folder.

What is a way to share a drop down inside a layout for use in all views?

I am becoming more familiar with MVC 3 and the RAZOR view engine. I have a question regarding layouts and shared controls on pages.
Let’s say I have a header section defined in my main layout. In that header is a dropdown I need to populate with project names. This dropdown will serve as a context for the entire site and is present on all pages. As an example, if the user selects “Project A” from the drop down, all of the views for the site will be based on “Project A”. Since this dropdown control is rather static and is used by the entire site, where is the best place to put the code to pull all the projects to display in the dropdown? In a Partial View? In a HTML helper? Another thought is, if a user selects a new value, they would be taken to a dashboard or similar page for that newly selected project. I am trying to figure out how to reuse this control on every page in the site without having to keep wiring it up in every possible controller.
You could use a child action along with the Html.Action helper. So you start by defining a view model:
public class ProjectViewModel
{
[DisplayName("Project name")]
public string ProjectId { get; set; }
public IEnumerable<SelectListItem> ProjectNames { get; set; }
}
then a controller:
public class ProjectsController: Controller
{
private readonly IProjectsRepository _repository;
public ProjectsController(IProjectsRepository repository)
{
_repository = repository;
}
public ActionResult Index(string projectId)
{
var projects = _repository.GetProjects();
var model = new ProjectViewModel
{
ProjectId = projectId,
ProjectNames = projects.Select(x => new SelectListItem
{
Value = x.Id,
Text = x.Name
})
};
return PartialView(model);
}
}
then the corresponding view (~/views/projects/index.cshtml):
#model ProjectViewModel
#Html.LabelFor(x => x.ProjectId)
#Html.DropDownListFor(
x => x.ProjectId,
Model.ProjectNames,
new {
id = "projects",
data_url = Url.Action("SomeAction", "SomeController")
}
)
Now all that's left is to render this widget inside the _Layout.cshtml:
#Html.Action("Index", "Products", new { projectid = Request["projectId"] })
And now we could put some javascript so that when the user decides to change the selection he is redirected to some other action:
$(function() {
$('#projects').change(function() {
var url = $(this).data('url');
var projectId = encodeURIComponent($(this).val());
window.location.href = url + '?projectid=' + projectId;
});
});
Another possibility is to put the dropdown inside an HTML form:
#model ProjectViewModel
#using (Html.BeginForm("SomeAction", "SomeController", FormMethod.Get))
{
#Html.LabelFor(x => x.ProjectId)
#Html.DropDownListFor(
x => x.ProjectId,
Model.ProjectNames,
new {
id = "projects",
}
)
}
so that inside the javascript we don't have to worry about building urls when the selection changes and simply trigger the containing form submission:
$(function() {
$('#projects').change(function() {
$(this).closest('form').submit();
});
});
We just did a similiar thing on a project.
First, you can't really put it in a section because you have to put that section on every view, you could put it in a partial but you would still have to call it from every view.
Second, you can't really put it in the Layout page because the layout page isn't passed any kind of model. So I created an html helper and referenced that in the layout page. There are lots of tutorials on creating html helpers so I won't put the code here. But essentially in your html helper you can make a database call to get all of your projects. Then you can create a select list using string builder in the html helper and return that to the layout page. We then used jquery to add an on change event to the select list. When the select list changed it loaded a new page. So for example, in your select list the value of each item could be the project id, then on change it redirects them to a page like /Projects/View?id=234 where 234 is your project id.
So things to research. 1. Creating HTML Helpers 2. JQUERY change event.
That should get you in the right direction. Let me know if you need any other help and I can post some code.

ASP.NET MVC Routes and Client Specifc Functions

I'm working on an application where a system is being built where multiple "customers" will use the system and 99.9% of the controllers/actions will be the same, just pulling different data, but there are times and places where a custom controller action, or view might be needed.
Right now I'm using a default route similar to the following to get the company name with requests.
routes.MapRoute(
"Default", // Route name
"{company}/{controller}/{action}/{id}",
new {company = "Unknown", controller = "Home",
action = "Index", id = UrlParameter.Optional}
);
This works great as I can have my individual controller actions defined like this
public ActionResult ShowReport(string company)
{
//Actual code goes here..
}
I have a system in place that will get the data segment for this specific company and return the proper view. SO for my 99.9% situation this looks great. What I'm looking for is a solution for when I need to render a different view, or have additional actions that are specific to one company.
I could add in switch or other logic within my action, but that feels overly dirty...
For a specific company you can use something like this and put it before the default action, in this case url has to contain Company1/somethingcontroller/etc/etc.
routes.MapRoute(
"Company1Default", // Route name
"Company1/{controller}/{action}/{id}",
new {company = "Company1", controller = "DefaultControllerForCompany1",
action = "Index", id = UrlParameter.Optional}
);
While I actually lean to Jay's answer pertaining to the use of data within models, I think that there is another option. Be warned that I haven't played this all the way out and don't have a full understanding of your application...
Why would you want to hardcode a company name within your global.asax? I don't think that it would be very scalable. If you want to add support for an additional 10 companies, you'd have to create 10 new entries. Also, what if you want to change the name of a company because of a buyout or something? More maintenance.
Why not add a route to send every company to the same controller like...
routes.MapRoute(
"CompanyRouting", // Route name
"{companyname}/{action}",
new { controller = "MySingleCompanyControllerName", action = "Index", companyname = UrlParameter.Optional }
);
MySingleCompanyController.cs
Once in your controller you can just get whatever the companyname value is whenever you want it.
public ActionResult Index()
{
ViewData["companynamevalue"] = RouteData.Values["companyname"];
return View();
}
Index.aspx
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
</head>
<body>
<div>
Requested Company Name = <%: ViewData["companynamevalue"] %>
</div>
</body>
</html>
NOTE: One more thing to look into for routing help is Phil Haack's routing debugger.
Defining which view is return from an action is easy:
return View("Index");
This will return the view named "Index" no matter which action is invoked.
On the other hand, both views and actions must be defined at compile time, so you cannot dynamically create them (from what i know of).
I might suggest implementing the Command Pattern in your action to get exactly what you would want for individual companies from a single action.

MVC3 Ajax.BeginForm odd behavior

I have a very simple view that has a DropDownListFor and a Button inside an Ajax.BeginForm helper. Clicking the button renders the whole view again inside the div I have set to update including the layout page (I also notice a spike in the cpu when clicking the button multiple times)
Here is the Ajax.BeginForm inside the view:
#using (Ajax.BeginForm("About2", "Home", new AjaxOptions { UpdateTargetId = "property22" }))
{
<div>
<div id="property22">
#Html.DropDownListFor(m => m.SomePropertyToBind, new SelectList(Model.list, "property1", "property2"))
</div>
<button type="submit" id="test">
Click me</button>
</div>
}
Any ideas where I'm going wrong?
I uploaded the whole project if someone has a couple of minutes to take a look at it:
http://www.sendspace.com/file/siu3r31 (free provider so there may be a popup)
Thanks
You are using a wrong overload of the Ajax.BeginForm helper. It should be like this:
#using (Ajax.BeginForm(
"About2",
"Home",
null,
new AjaxOptions { UpdateTargetId = "property22" },
new { #id = "refreshe" }
))
Notice the additional null value I am passing as the routeValues parameter. Also in the example you uploaded you forgot to include the TestView.cshtml view. This being said in order to fix the problem you have two possibilities:
Either return a partial view:
public ActionResult About2()
{
Random randomizer = new Random();
int random = randomizer.Next(1, 1000000000);
ModelTest newModelTest = new ModelTest();
string[] stringList = new string[] { "Red", "Blue", "Green" };
newModelTest.list = from test in stringList
select new ModelTestList
{
property1 = test,
property2 = test
};
newModelTest.SomePropertyToBind = stringList[random % 2];
return PartialView("TestView", newModelTest);
}
or disable the layout in the TestView.cshtml view:
#{
Layout = null;
}
Unfortunately from your explanation above and from the code, I am not sure what you are trying to achieve. However, I think your most worry is about having Ajax working in your view.
In your About2 action method, you are trying to return a complete view which is TestView (in that case, it doesnt exist) and passing it the newModelTest view Model. I would advise changing to return either a PartialView or JsonResult.
For example, changing the return statement of About2 action method to
public ActionResult About2()
{
...
return Json(newModelTest);
}
or changing it to a return type to string and returning "TestResult"
public String About2()
{
...
return "TestResult";
}
or you could change the return statement to return a PartialView
Thanks for your replies.
I just realized that About2 should have returned the "About" view instead of the "TestView". I had tried creating a partial view with the Ajax.BeginForm code but I came across the same problem.
This is my first attempt at Ajax.BeginForm (so far I have always used jquery), and I was under the impression that it works in a similar fashion in the sense that by specifying the target id only the contents of that element will get updated, not that the target will actually get replaced by the whole response object.
Thanks for your help, not only did I get it to work, but I now understand how it should work.
I suspect that what's happening is that you're returning the a complete View (including the layout template) in the Ajax response. Try changing your "Home" controller "About2" action temporarily to the following:
public ContentResult About2() {
return Content("Hello World");
}
I tested this sample Action with your Razor markup and clicking the button properly replaced your dropdown list with "Hello World!".
If this is indeed what's happening, then you'll want to return a View from "About2" without the layout by declaring the following at the top of the View that you're returning.
#{
Layout = null;
}

Resources