views:

58

answers:

2

From the SEO standpoint it is nice to see urls in format which explains what is located on a page Let's have a look on such situation (it is just example) We need to display page about some product and decided to have such url template for that page: /product/{ProductId}/{ProductCategory}/{ProductUrlName}. And create for this purpose such model

public class ProductUrlInfo{
   public int ProductId{get;set;}
   public string ProductCountry{get;set;}
   public string ProductUrlName{get;set;}
}

I want to create controller method where I pass ProductUrlInfo object but not all required fields. Classic controller method for url template shown above is following

public ActionResult Index(int ProductId, string ProductCategory, string ProductUrlName){
    return View();
}

and we need to call it like that Html.ActionLink<UserController>(x=>Index(user.ProductId, user.ProductCategory, user.ProductUrlName), "See user page")

I want to create such controller method

public ActionResult Index(ProductUrlInfo productInfo){
    return View();
}

and call it like that: Html.ActionLink<ProductController>(x=>Index(product), "See product page")

Actually I works when we add one more route and point it to the same controller method, so routing will be: /product/{productInfo} /product/{ProductId}/{ProductCategory}/{ProductUrlName} In this situation routing engine gets string method of our model (need to override it) and it works ALMOST always. But sometimes it fails and show url like /page/?productInfo=/Cars/Porsche911

So my workaround does not always work properly. Does anybody know how to work with urls in such way?

Edit Maybe it was unclear, sorry... ProductUrlInfo is NOT a view model. That is an object which created just to be shown in the url ONLY. Example of all object for such product

public class ProductList: List<Product>{}

public class Product{
   public int ProductId{get;set;}
   public string ProductCountry{get;set;}
   public string ProductName{get;set;}
   public string Height{get;set;}
   public float Price{get;set;}

   //data which must be rendered in the url string
   public ProductUrlInfo Key(){
      return new ProductUrlInfo(){
        ProductId = this.ProductId
        ,ProductCountry = MyConverter.EncodeForUrl(this.ProductCountry)
        ,ProductUrlName= MyConverter.EncodeForUrl(this.ProductName)
      }
   }

}

Somewhere on a View control:

foreach( var pr in Model) 
  Html.RenderActionLink<ProductController>(x=>Index(pr.Key), "See product page")

Controller methods should be like this:

//accept our complex object as a key
public ActionResult Index(ProductUrlInfo key){
    //retrive data from database or any other stuff
    Product pr = Repository.GetProductByKey(key);
    return View(pr);
}

//accept our complex object as a key
public ActionResult Edit(ProductUrlInfo key){
    //retrive data from database or any other stuff
    Product pr = Repository.GetProductByKey(key);
    return View(pr);
}

[HttpPost]
public ActionResult Edit(Product product){
    //do update here
}

Let me explain how it passed to Index(ProductUrlInfo key) controller method. I think that I'm using some side effect. But at first if you have several items you want to pass to controller and actually only single item is object primary key (i.e. object ID) and other elements just explain user which page he has opened (some additional information) (i.e. object category and name). So in a future you might want to change this information (add new fields/ remove old etc.) But if you have links to that page from all over the project then it may be quite painful to completely replace links to new format. So why not pass to controller some model which then be rendered into url? I investigated that if you can do following steps to pass custom object to controller 1. create class which contains all required fields to be shown in url (ProductUrlInfo class here)

  1. register route with your actually passed object (in my particular case it is "/{key}" )

  2. here some magic. Register AFTER that your desired url format routes ("/{id}/{Category}/{ProductUrlName}" and for example "/{id}/{ProductUrlName}") You might want to render only name if it is unknown its category for some reason

  3. override ToString method in ProductUrlInfo class and it must render your desired url format. i.e.

    public override string ToString()
    {
    return (!String.IsNullOrEmpty(Category))
    ? string.Format( "{0}/{1}/{2}", Id, Category, ProductUrlName )
    : string.Format( "{0}/{1}", Id, ProductUrlName ); }

    As I do understand that effect: when routing engine gets passed object it looks for appropriate route and found {key} string. Then it found that passed object is a complex type and calls its ToString method. But its not blindly set result instead of {key} parameter but also compares it against routes. Now if you pass ProductUrlInfo into ActionLink as a parameter you will be tranfered to Index(ProductUrlInfo key) controller method. Of course it looks for me as a hack so I'd like to know is there anybody who passes objects into GET controller method but in some other (better) way?

A: 

Just a suggestion.

It would be poor practice, in my opinion, to have userID, userCountry and userLogin within the URL string. These appear to be user information rather than application information, there may seem to be little difference but I believe there is a difference and it's important.

If I have an authenticated user (as differentiated from a logged in user, an authenticated user can be anonymous providing the site allows anonymous users) then I'd hold their ID, country, login information in the session as it would be potentially insecure to reveal that data within a URL and also because it wouldn't be relevant for SEO unless you were looking to dilute the importance of each page in the site. While the site might present the same page to every user, the SE would see hundreds (as many as you have users) of URLs pointing to it.

I think it'll be much simpler for you to store that information within a session state and keep your URLs even clear and purely focused on identifying your users desired actions.

Lazarus
Lazarus, thank you for you suggestion but that's a sample model - the first thing came into my head. Actually I do not work with users in such way. Look at it as on product. Yes, I will change samples in the header.
Cheburek
You should identify the product by a single key and anything else is a filter which should be dealt with separately.
Lazarus
Yes, ProductId is unique key and all other fields just user information. I.e. see address bar of the current page. You see /questions/3061430/how-to-convert-model-into-url-properly-in-asp-net-mvc where 3061430 is a pageId and "how-to-convert-model-into-url-properly-in-asp-net-mvc" - info for user. I wanna do the same thing but wrap this info into separate object - in case if in future I should want change url format...
Cheburek
A: 

You need to urlencode your strings. Try changing the ProductUrlInfo class to smth. like this:

public class ProductUrlInfo{

  public int ProductId{ get; set;}
  public string ProductCountry{ get; set; }

  private string productUrlName;
  public string ProductUrlName
  {
      get { return HttpUtility.UrlEncode(productUrlName); }
      set { productUrlName = HttpUtility.UrlDecode(value); }
  }
}

However, this isn't considered to be a good practice to work with objects in ASP.NET MVC this way. Instead, you could use a form

<% using (Html.BeginForm("Index", "User", FormMethod.Post))
{ %>

   <%= Html.HiddenFor(model => model.ProductId) %>
   <%= Html.HiddenFor(model => model.ProductCategory) %>
   <%= Html.HiddenFor(model => model.ProductUrlName) %>

   <input type="submit" value="See product page" />

<% } %>

(The submit button is for simplicity, you could use a link that would submit the form via js instead)

You could also create a separate Index action with an HttpPost attribute to distinguish the cases when one comes to the index page for the first time and when one comes via the link/button (if necessary):

[HttpPost]
public ActionResult Index(ProductUrlInfo productInfo){
  return View();
}

And the route doesn't need to include ProductUrlInfo in this case.

EDIT: Just to be clear, Lazarus is mostly right and for your case you shouldn't be doing what you are doing. An object must be identified by one key, otherwise you can come across inconsistency if someone messes around with your URL (manually in the address bar). E.g. changes the category to something that the product doesn't belong to.

Yakimych
ProductUrlInfo object created just to show it in the url. It is NOT a view model. Actually page has another model.
Cheburek
Sorry if it is unclear. I've update original post where tried to clarify 'model' things
Cheburek