How to create an ActionLink with Properties for the View Model - asp.net-mvc

I have a ViewModel with a Filter property that has many properties that I use to filter my data
Example:
class MyViewModel : IHasFilter
{
public MyData[] Data { get; set; }
public FilterViewModel Filter { get; set; }
}
class FilterViewModel
{
public String MessageFilter { get; set; }
//etc.
}
This works fine when using my View. I can set the properties of Model.Filter and they are passed to the Controller. What I am trying to do now, is create an ActionLink that has a query string that works with the above format.
The query string generated by my View from above looks like this:
http://localhost:51050/?Filter.MessageFilter=Stuff&Filter.OtherProp=MoreStuff
I need to generate an ActionLink in a different View for each row in a grid that goes to the View above.
I have tried:
Html.ActionLink(
item.Message,
"Index",
"Home",
new { Filter = new { MessageFilter = item.Message, }, },
null);
I also tried setting the routeValues argument to:
new MyViewModel { Filter = new FilterViewModel { MessageFilter = item.Message, }, },
But these do not generate the query string like the above one.

Interesting question (+1). I'm assuming that the purpose is to use the default model binder to bind the querystring parameters to to your Action parameters.
Out of the box I do not believe that the ActionLink method will do this for you (of course there is nothing stopping you from rolling your own). Looking in reflector we can see that when the object is added to the RouteValueDictionary, only key value pairs are added. This is the code that adds the key value pairs and as you can see there is no traversing the object properties.
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
{
object obj2 = descriptor.GetValue(values);
this.Add(descriptor.Name, obj2);
}
So for your object
var values = new { Filter = new Filter { MessageFilter = item.Message } }
the key being added is Filter and the value is your Filter object which will evaluate to the the fully qualified name of your object type.
The result of this is Filter=Youre.Namespace.Filter.
Edit possible solution depending on your exact needs
Extension Method does the work
Note that it uses the static framework methods ExpressionHelper and ModelMetadata (which are also used by the existing helpers) to determine the appropriate names that the default model binder will understand and value of the property respectively.
public static class ExtentionMethods
{
public static MvcHtmlString ActionLink<TModel, TProperty>(
this HtmlHelper<TModel> helper,
string linkText,
string actionName,
string controllerName,
params Expression<Func<TModel, TProperty>>[] expressions)
{
var urlHelper = new UrlHelper(helper.ViewContext.HttpContext.Request.RequestContext);
var url = urlHelper.Action(actionName, controllerName);
if (expressions.Any())
{
url += "?";
foreach (var expression in expressions)
{
var result = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, helper.ViewData);
url = string.Concat(url, result, "=", metadata.SimpleDisplayText, "&");
}
url = url.TrimEnd('&');
}
return new MvcHtmlString(string.Format("<a href='{0}'>{1}</a>", url, linkText));
}
}
Sample Models
public class MyViewModel
{
public string SomeProperty { get; set; }
public FilterViewModel Filter { get; set; }
}
public class FilterViewModel
{
public string MessageFilter { get; set; }
}
Action
public ActionResult YourAction(MyViewModel model)
{
return this.View(
new MyViewModel
{
SomeProperty = "property value",
Filter = new FilterViewModel
{
MessageFilter = "stuff"
}
});
}
Usage
Any number of your view model properties can be added to the querystring through that last params parameter of the method.
#this.Html.ActionLink(
"Your Link Text",
"YourAction",
"YourController",
x => x.SomeProperty,
x => x.Filter.MessageFilter)
Markup
<a href='/YourAction/YourController?SomeProperty=some property value&Filter.MessageFilter=stuff'>Your Link Text</a>
Instead of using string.Format you could use TagBuilder, the querystring should be encoded to be safely passed in a URL and this extension method would need some additional validation but I think it could be useful. Note also that, though this extension method is built for MVC 4, it could be easily modified for previous versions. I didn't realize that that one of the MVC tags was was for version 3 until now.

You could create one RouteValueDictionary from a FilterViewModel instance and then use ToDictionary on that to pass to another RouteValues with all the keys prefixed with 'Filter.'.
Taking it further, you could construct a special override of RouteValueDictionary which accepts a prefix (therefore making it more useful for other scenarios):
public class PrefixedRouteValueDictionary : RouteValueDictionary
{
public PrefixedRouteValueDictionary(string prefix, object o)
: this(prefix, new RouteValueDictionary(o))
{ }
public PrefixedRouteValueDictionary(string prefix, IDictionary<string, object> d)
: base(d.ToDictionary(kvp=>(prefix ?? "") + kvp.Key, kvp => kvp.Value))
{ }
}
With that you can now do:
Html.ActionLink(
item.Message,
"Index",
"Home",
new PrefixedRouteValueDictionary("Filter.",
new FilterViewModel() { MessageFilter = item.Message }),
null);
The caveat to this, though, is that the Add, Remove, TryGetValue and this[string key] methods aren't altered to take into account the prefix. That can be achieved by defining new versions of those methods, but because they're not virtual, they'd only work from callers that know they're talking to a PrefixedRouteValueDictionary instead of a RouteValueDictionary.

Related

Asp.Net mvc 5 - How can I pass a complex object as route value in Html.ActionLink() so default model binder can map it?

I have an object containing searching, sorting and paging parameters as well as an id of a record to be edited.
I'd like to pass this object into Html.ActionLink() as a route value object, so that the resulting query string will be correctly mapped by the default model binder into the Edit action's parameter, which is an EditViewModel.
The idea is that after the Edit action completes, it can redirect back to the Index and maintain the same paging/sorting position, in the same data set, and filtered by the same search string.
Edit View Model:
public class EditViewModel
{
public SearchSortPageViewModel SearchSortPageParams { get; set; }
public int Id { get; set; }
public EditViewModel()
{
SearchSortPageParams = new SearchSortPageViewModel();
Id = 0;
}
public EditViewModel(SearchSortPageViewModel searchSortPageParams, int id)
{
SearchSortPageParams = searchSortPageParams;
Id = id;
}
}
public class SearchSortPageViewModel
{
public string SearchString { get; set; }
public string SortCol { get; set; }
public string SortOrder { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
}
Edit action:
public ActionResult Edit(EditViewModel evm)
{
/* ... */
}
When I do this in the view:
#model MyApp.Areas.Books.ViewModels.Books.IndexViewModel
...
#{EditViewModel evm = new EditViewModel(Model.SearchSortPageParams, item.ID);}
#Html.ActionLink("Edit", "Edit", evm)
I get this:
http://localhost:63816/Books/Books/Edit/4?SearchSortPageParams=MyApp.Areas.Base.ViewModels.SearchSortPageViewModel
But I want this:
http://localhost:63816/Books/Books/Edit/4?SearchSortPageParams.SearchString=abc&SearchSortPageParams.SortCol=name&SearchSortPageParams.SortOrder=asc&SearchSortPageParams.Page=1&SearchSortPageParams.PageSize=3
The only way I have been able to pass the object so far has been to manually prepare the query string, like this:
#{string theQueryString = "?SearchSortPageParams.SearchString=" + #evm.SearchSortPageParams.SearchString + "&SearchSortPageParams.SortCol=" + #evm.SearchSortPageParams.SortCol + "&SearchSortPageParams.SortOrder=" + #evm.SearchSortPageParams.SortOrder + "&SearchSortPageParams.Page=" + #evm.SearchSortPageParams.Page + "&SearchSortPageParams.PageSize=" + #evm.SearchSortPageParams.PageSize;}
Edit
I thought of writing a custom model binder, but it seems silly given that the default model binder already handles nested objects if formatted as a query string in the way it expects.
I also thought of writing a custom object serializer which outputs a serial format which the default model binder expects, but haven't yet gone down that route.
Finally, I thought of flattening out the EditViewModel so there is nothing nested, just have all the properties listed out flatly. But, it's not ideal.
So, what is the best way to go?
As far as I know, you can't pass the complex object directly, but you can avoid having to build the query string yourself by passing a RouteValueDictionary:
#Html.ActionLink("Edit", "Edit", new RouteValueDictionary {
{"SearchSortPageParams.SortOrder", evm.SearchSortPageParams.SortOrder },
{ /* etc... */ }
})
This should generate the query string as you need it.
The only other alternative would be use reflection to iterate over the properties of the model and generate this dictionary that way but that would, in my opinion, be over-engineered.
Of course, I would generally suggest in this situation that you just have your action method take separate parameters:
public ActionResult Search(string searchString, SortOrder sortOrder, ...)
I'd generally consider this to be a more appropriate way to pass GET parameters to a method (of course, this could get unwieldy if you have a lot of parameters). Then you can just do the following, which is much tidier:
#Html.ActionLink("Edit", "Edit",
new { sortOrder = evm.SearchSortPageParams.SortOrder, ... })

How can I pass the current URL params to method for replacement

I am using the Pager HTML Helper class from twitter.bootstrap.mvc. It will build list of page links for Bootstrap.
How would I pass the current URL parameters to this function?
public static MvcHtmlString Pager(this HtmlHelper helper,
int currentPage, int totalPages,
Func<int, string> pageUrl, <-- This
string additionalPagerCssClass = "") {
...
a.MergeAttribute("href", pageUrl(i));
...
}
It is called like this:
#Html.Pager(Model.PageIndex, Model.TotalPages, x => Url.Action("Results", "Search", new { page = x }))
The x => Url.Action("Results", "Search", new { page = x }) is the part that I don't know how to change. This is a search results page and has the search settings in the URL as parameters. For the paging to work, I need these params.
Is my only option to specify every single param and have them in the ViewModel as well as the URL?
Is my only option to specify every single param and have them in the
ViewModel as well as the URL?
It is probably not your only option, you could look at using ViewData or cookies for example, but it is probably the best option.
The ViewModel should really contain all the data required to render your view. Given that your paging controls depend on this data, it is not unreasonable for it to be part of your ViewModel.
You View will still look reasonably tidy
e.g.
`#Html.Pager(Model.PageIndex, Model.TotalPages, x => Url.Action("Results", "Search", new { page = x, search = Model.Search, orderBy = Model.OrderBy }))`
If you have a lot of parameters t pass, you could enhance this by making a special "Paging" class which will hold all the details required for the paging, have this as a property of your model modify the HtmlHelper to accept this, to tidy up some of this code.
e.g.
public class PagingDetails
{
public int TotalPages { get; set; }
public int CurrentPage { get; set; }
public int TotalResults { get; set; }
public string Search { get; set; }
public string OrderBy { get; set; }
...other parameters...
}
public static MvcHtmlString Pager(this HtmlHelper helper,
Func<int, string> pageUrl,
PagingDetails pagingDetails,
string additionalPagerCssClass = "")
{
// need to tweak this to append extra parameters to resulting URL
}
`#Html.Pager(x => Url.Action("Results", "Search", new { page = x }, Model.PagingDetails))`

EF 4: Referencing Non-Scalar Variables Not Supported

I'm using code first and trying to do a simple query, on a List property to see if it contains a string in the filtering list. However I am running into problems. For simplicity assume the following.
public class Person
{
public List<string> FavoriteColors { get; set; }
}
//Now some code. Create and add to DbContext
var person = new Person{ FavoriteColors = new List<string>{ "Green", "Blue"} };
dbContext.Persons.Add(person);
myDataBaseContext.SaveChanges();
//Build
var filterBy = new List<string>{ "Purple", "Green" };
var matches = dbContext.Persons.AsQueryable();
matches = from p in matches
from color in p.FavoriteColors
where filterBy.Contains(color)
select p;
The option I am considering is transforming this to a json serialized string since I can perform a Contains call if FavoriteColors is a string. Alternatively, I can go overboard and create a "Color" entity but thats fairly heavy weight. Unfortunately enums are also not supported.
I think the problem is not the collection, but the reference to matches.
var matches = dbContext.Persons.AsQueryable();
matches = from p in matches
from color in p.FavoriteColors
where filterBy.Contains(color)
select p;
If you check out the Known Issues and Considerations for EF4 this is more or less exactly the case mentioned.
Referencing a non-scalar variables,
such as an entity, in a query is not
supported. When such a query executes,
a NotSupportedException exception is
thrown with a message that states
"Unable to create a constant value of
type EntityType.
Also note that it specifically says that referencing a collection of scalar variables is supported (that's new in EF 4 imo).
Having said that the following should work (can't try it out right now):
matches = from p in dbContext.Persons
from color in p.FavoriteColors
where filterBy.Contains(color)
select p;
I decided to experiment by creating a "StringEntity" class to overcome this limitation, and used implicit operators to make nice easy transformations to and from strings. See below for solution:
public class MyClass
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid Id { get; set; }
public List<StringEntity> Animals { get; set; }
public MyClass()
{
List<StringEntity> Animals = List<StringEntity>();
}
}
public class StringEntity
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid Id { get; set; }
public string Value { get; set; }
public StringEntity(string value) { Value = value; }
public static implicit operator string(StringEntity se) { return se.Value; }
public static implicit operator StringEntity(string value) { return new StringEntity(value); }
}
public class MyDbContext : DbContext
{
public DbSet<MyClass> MyClasses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyClass>()
.HasMany(x => x.Animals)
.WithMany()
.Map(x =>
{
x.MapLeftKey(l => l.Id, "MyClassId");
x.MapRightKey(r => r.Id, "StringEntityId");
});
}
}
...Everything looked like it was working perfectly with some testing(Albeit heavy), and then I implemented for its original purpose, a Multiselect ListBox in an MVC3 view. For reasons unknown to me, IF the ListBox is assigned the same NAME as an Entity Collection Property, none of your selected items will be loaded.
To demonstrate the following did NOT work:
//Razor View Code
string[] animalOptions = new string[] {"Dog", "Cat", "Goat"};
string[] animalSelections = new string[] {"Dog", "Cat"};
Html.ListBox("Animals", Multiselect(animalOptions, animalSelections));
To get around this limitation, I needed to do four things:
//#1 Unpluralize the ListBox name so that is doesn't match the name Model.Animals
var animalOptions = new string[] {"Dog", "Cat", "Goat"};
#Html.ListBox("Animal", new MultiSelectList(animalOptions, Model.Animals.Select(x => x.Value)))
//#2 Use JQuery to replace the id and name attribute, so that binding can occur on the form post
<script type="text/javascript">
jQuery(function ($) {
$("select#Animal").attr("name", "Animals").attr("id", "Animals");
});
</script>
//#3 Create a model binder class to handle List<StringEntity> objects
public class StringEntityListBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringArray = controllerContext.HttpContext.Request.Params.GetValues(bindingContext.ModelName);
return stringArray.Select(x => new StringEntity(x)).ToList();
}
}
//#4 Initialize the binder in your Global.asax setup.
ModelBinders.Binders.Add(typeof(List<StringEntity>), new StringEntityListBinder ());
Note, that the Listbox bug did NOT occur when the property was a List of strings, it just didn't like it when it was a List of entities.

HtmlHelper building a dropdownlist, using ServiceLocator : code smell?

Is it a code smell to have to following pattern, given the following code (highly simplified to get straight to the point) ?
The models :
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Category Cat { get; set; }
}
class Category
{
public int Id { get; set; }
public string Label { get; set; }
}
The view to edit a Product :
<% =Html.EditorFor( x => x.Name ) %>
<% =Html.EditorFor( x => x.Category ) %>
The EditorTemplate for Category
<% =Html.DropDownList<Category>() %>
The HtmlHelper method
public static MvcHtmlString DropDownList<TEntity>(this HtmlHelper helper)
where TEntity : Entity
{
var selectList = new SelectList(
ServiceLocator.GetInstance<SomethingGivingMe<TEntity>>().GetAll(),
"Id", "Label");
return SelectExtensions.DropDownList(helper, "List", selectList, null, null);
}
For information, the real implementation of the helper method takes some lambdas to get the DataTextField and DataValueField names, the selected value, etc.
The point that bothers me is using a servicelocator inside the HtmlHelper. I think I should have a AllCategories property in my Product model, but I would need to be populated in the controller every time I need it.
So I think the solution I'm using is more straightforward, as the helper method is generic (and so is the modelbinder, not included here). So I just have to create an EditorTemplate for each type that needs a DropDownList.
Any advice ?
IMHO I'd leave it the way it is, have the same thing in another project.
BUT the service location bothered me as well so for another project I made this part of an ActionFilter which scans a model, finds all the anticipated dropdowns and does a batch load into ViewData. Since the ServiceLocator or Repository/Context/whatever is already injected into the Controller you don't have to spread your service location all over the place.
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
foreach( var anticipated in SomeDetectionMethod() )
{
var selectList = new SelectList(
ServiceLocator.GetInstance<SomethingGivingMe<TEntity>>().GetAll(),
"Id", "Label");
ViewData["SelectList." + anticipated.Label/Name/Description"] = selectList;
}
}
In the view you can then make a helper to load up those dropdowns via a custom editor template or other method.
advice: look at the asp.net mvc sample application from here: http://valueinjecter.codeplex.com/
good luck ;)
This is how ValueInjecter's Sample Application could get the dropdowns:
(but it doesn't right now cuz I'm ok with the Resolve thing)
public class CountryToLookup : LoopValueInjection<Country, object>
{
ICountryRepo _repo;
public CountryToLookup(ICountryRepository repo)
{
_repo = repo;
}
protected override object SetValue(Country sourcePropertyValue)
{
var value = sourcePropertyValue ?? new Country();
var countries = _repo.GetAll().ToArray();
return
countries.Select(
o => new SelectListItem
{
Text = o.Name,
Value = o.Id.ToString(),
Selected = value.Id == o.Id
});
}
}

Is there simple way how to join two RouteValueDictionary values to pass parameters to Html.ActionLink

Look on my code that i created in a partial View:
<% foreach (Customer customerInfo in Model.DataRows) {%>
<tr>
<td>
<%=Html.ActionLink(
customerInfo.FullName
, ((string)ViewData["ActionNameForSelectedCustomer"])
, JoinParameters(customerInfo.id, (RouteValueDictionary) ViewData["AdditionalSelectionParameters"])
, null)%>
</td>
<td>
<%=customerInfo.LegalGuardianName %>
</td>
<td>
<%=customerInfo.HomePhone %>
</td>
<td>
<%=customerInfo.CellPhone %>
</td>
</tr>
<%}%>
Here I'm building simple table that showing customer's details.
As you may see, in each row, I'm trying to build a link that will redirect to another action.
That action requires customerId and some additional parameters.
Additional parameters are different for each page where this partial View is using.
So, i decided to make Action methods to pass that additional parameters in the ViewData as RouteValueDictionary instance.
Now, on the view i have a problem, i need to pass customerId and that RouteValueDictionary together into Html.ActionLink method.
That makes me to figure out some way of how to combine all that params into one object (either object or new RouteValueDictionary instance)
Because of the way the MVC does, i can't create create a method in the codebehind class (there is no codebihind in MVC) that will join that parameters.
So, i used ugly way - inserted inline code:
...script runat="server"...
private RouteValueDictionary JoinParameters(int customerId, RouteValueDictionary defaultValues)
{
RouteValueDictionary routeValueDictionary = new RouteValueDictionary(defaultValues);
routeValueDictionary.Add("customerId", customerId);
return routeValueDictionary;
}
...script...
This way is very ugly for me, because i hate to use inline code in the View part.
My question is - is there any better way of how i can mix parameters passed from the action (in ViewData, TempData, other...) and the parameter from the view when building action links.
May be i can build this link in other way ?
Thanks!
Write an extension method to merge source dictionary into the destination.
public static class Util
{
public static RouteValueDictionary Extend(this RouteValueDictionary dest, IEnumerable<KeyValuePair<string, object>> src)
{
src.ToList().ForEach(x => { dest[x.Key] = x.Value; });
return dest;
}
public static RouteValueDictionary Merge(this RouteValueDictionary source, IEnumerable<KeyValuePair<string, object>> newData)
{
return (new RouteValueDictionary(source)).Extend(newData);
}
}
usage:
Url.Action(actionName, controllerName, destRvd.Extend(srcRvd));
If you instead want a 3rd dictionary containing the contents of the two - use Merge. But that is less efficient. This approach enables more or less efficient chaining of the merges if you need to merge more than 2 dictionaries:
Url.Action(actionName, controllerName, destRvd.Extend(srcRvd1).Extend(srcRvd2));
Efficiently merge 3 dictionaries into a new one:
Url.Action(actionName, controllerName, srcRvd1.Merge(srcRvd2).Extend(srcRvd3));
Yes - you can merge two (or more) RouteValueDictionary objects together using the Union method. Microsoft has good example code of the Union method here but Microsoft isn't really clear when it comes to using RouteValueDictionary Unions (hence the reason for my post).
Given the following classes...
public class Student
{
public int StudentID { get; set; }
public string FirstName { get; set; }
public string LastNamse { get; set; }
public string Email { get; set; }
public DateTime DateOfBirth { get; set; }
}
public class Address
{
public string StreetLine1 { get; set; }
public string StreetLine2 { get; set; }
public string Suburb { get; set; }
public string State { get; set; }
public string PostCode { get; set; }
}
...and some sample code...
Student aStudent = new Student();
aStudent.StudentID = 1234;
aStudent.FirstName = "Peter";
aStudent.LastNamse = "Wilson";
aStudent.Email = "peterwilson_69#example.com";
aStudent.DateOfBirth = new DateTime(1969, 12, 31);
Address homeAddr = new Address();
homeAddr.StreetLine1 = "Unit 99, Floor 10";
homeAddr.StreetLine2 = "99-199 Example Street";
homeAddr.Suburb = "Sydney";
homeAddr.State = "NSW";
homeAddr.PostCode = "2001";
Initialize and add the KeyValue<string, object> elements to the RouteValueDictionary objects:
RouteValueDictionary routeStudent = new RouteValueDictionary(aStudent);
RouteValueDictionary routeAddress = new RouteValueDictionary(homeAddr);
...Merge the two dictionary objects using the Union operator:
RouteValueDictionary routeCombined = new RouteValueDictionary(routeStudent.Union(routeAddress).ToDictionary(k => k.Key, k => k.Value));
You can now pass the routeCombined variable to the appropriate method (such as ActionLink).
#Html.ActionLink("Click to pass routeValues to the RouteAPI", "MyActionName", routeCombined);
Writing out to the debug window...
foreach (KeyValuePair<string, object> n in routeCombined)
System.Diagnostics.Debug.WriteLine(n.Key + ": " + n.Value);
...will produce the following:
StudentID: 1234
FirstName: Peter
LastNamse: Wilson
Email: peterwilson_69#example.com
DateOfBirth: 31/12/1969 12:00:00 AM
StreetLine1: Unit 99, Floor 10
StreetLine2: 99-199 Example Street
Suburb: Sydney
State: NSW
PostCode: 2001
Perhaps a simple way to do accomplish this would be to create a UrlHelper extension that does something similar to what your JoinParameters method does. Something like the method below is a bit more reusable:
public static RouteValueDictionary AppendRouteValue(this UrlHelper helper, RouteValueDictionary routeValues, string key, object value)
{
RouteValueDictionary routeValueDictionary = new RouteValueDictionary(routeValues);
routeValueDictionary.Add(key, value);
return routeValueDictionary;
}
Now your action like would be something like:
<%=Html.ActionLink(
customerInfo.FullName
, ((string)ViewData["ActionNameForSelectedCustomer"])
, Url.AppendRouteValue((RouteValueDictionary) ViewData["AdditionalSelectionParameters"], "customerId", customerInfo.id)
, null)%>

Resources