Is it possible to map the Url of child nodes to custom data?
I am connecting to an external database to retrieve products. I have pages for each product, but I am not sure how to get the map the Url to the products returned in the Custom Model. This is what my view currently looks like:
It does inherit from RenderModel, so I do have acccess to the Umbraco extras. I was thinking of creating a table in my external db that would hold the page id's and link them with the products, but this is a last resort since it would require some extra maintenance. Is there another option?
What I think you are looking for is a way to map your products to virtual nodes within the CMS yeah?
I.E To be able to list pages for each product without having to create a page for each product in the back office?
If so, you should use an UmbracoVirtualNodeRouteHandler to create a custom route for your product pages. There's an excellent article on Shannon's blog about this functionality here but I'll also summarize it with an example of how I'd do it should the URL change.
If I'm way off bat here, sorry...
First you would set up a generic document type with matching template in the back office. Lets call it Product. A single page of this document type will be used to display each individual product virtually.
You then need to create an implementation of UmbracoVirtualNodeRouteHandler to tell Umbraco how to map the route to the page.
/// <summary>
/// The product virtual node route handler.
/// </summary>
public class ProductVirtualNodeRouteHandler : UmbracoVirtualNodeRouteHandler
{
/// <summary>
/// returns the <see cref="IPublishedContent"/> associated with the route.
/// </summary>
/// <param name="requestContext">
/// The request context.
/// </param>
/// <param name="umbracoContext">
/// The umbraco context.
/// </param>
/// <returns>
/// The <see cref="IPublishedContent"/>.
/// </returns>
protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
{
UmbracoHelper helper = new UmbracoHelper(umbracoContext);
string alias = "Product";
return helper.TypedContentAtRoot()
.First()
.Descendants()
.First(d => d.DocumentTypeAlias.InvariantEquals(alias));
}
}
Also create a custom route you can do this via inheriting the ApplicationEventHandler class.
/// <summary>
/// The custom route handler for assigning custom routes.
/// </summary>
public class CustomRouteRegistration : ApplicationEventHandler
{
/// <summary>
/// Boot-up is completed, this allows you to perform any other boot-up logic required for the application.
/// Resolution is frozen so now they can be used to resolve instances.
/// </summary>
/// <param name="umbracoApplication">The current <see cref="UmbracoApplicationBase"/></param>
/// <param name="applicationContext">The Umbraco <see cref="ApplicationContext"/> for the current application.</param>
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
RouteTable.Routes.MapUmbracoRoute(
"ProductPage",
"Products/Product/{id}/",
new
{
controller = "Product",
action = "GetProduct"
},
new ProductVirtualNodeRouteHandler());
}
}
Now it's time to create a controller. In there you will grab the product from your external database and map it to your view model.
/// <summary>
/// The product page controller.
/// </summary>
public class ProductController : RenderMvcController
{
/// <summary>
/// Renders an individual product page.
/// </summary>
/// <param name="model">
/// The <see cref="RenderModel"/> representing the current Umbraco node.
/// </param>
/// <param name="id">
/// The id of the product.
/// </param>
/// <returns>
/// The <see cref="ActionResult"/> encapsulating the result of an action method that is used to
/// perform a framework-level operation on behalf of the action method.
/// </returns>
public ActionResult GetProduct(RenderModel model, int id)
{
// Do your logic here to pull the product from the external database and map it to your
// custom viewmodel
return this.View("Product", viewmodel);
}
}
Umbraco should now be able to find all the products by url. You will also now also be able to use Url.Action() to generate the correct url to your products since you have created a custom route.
Hope that helps
James
P.S I wrote this without the IDE so I dunno if it compiles. It should :)
I have been playing around with the code you gave me and you read my mind. This is exactly what I was looking for. I did implement it and everything worked fine. I am getting an error now, but I also have a few questions:
Does a Product node have to exist in the back office or is a DocumentType and Template enough?
I wanted something better more friendly that /products/1, so I just ended up adding {name} to my route as well. Do you know if there is a way to avoid having the {id} parameter in there, but still have access to it? Also, {name} could have illegal characters in it, so I created a helper to strip those out unless Umbraco has something to handle this?
Where do you normally put the custom route handler and route mapper classes? I currently have them in the root of my website.
OK, on to the error message. Originally I tested the code with no Masterpage and with a model that does not inherit from RenderModel and everything works fine. Now when I include aMasterpage and inherit from RenderModel, when it gets to the following line:
var helper = new UmbracoHelper(umbracoContext); in FindContent, it throws the following error:
Value cannot be null.
Parameter name: umbracoContext
Oddly though, have it throws it a few times, it still renders the Product Page, even with an exception.
I found this snippet of code and now I don't get the error, but I don't understand what it does and why I don't get the error anymore. I added umbracoContext = UmbracoContext.EnsureContext(new HttpContextWrapper(HttpContext.Current), ApplicationContext.Current); to my FindContent method.
Normally that umbracoContext parameter shouldn't be null. That method is ensuring that you have an UmbracoHelper available to query the published content cache for your node and I would much rather we figured out why the code I supplied doesn't work as it should than add the additional code. Did you set up the handlers exactly as I described? Can I see how your controller is coded?
To answer your questions though.
Yes. A node of that document type does have to exist in the back office. We need something to pin our virtual product nodes to.
I wouldn't remove the id from your path. There should be a primary key for your product data so that any lookups are fast. Adding the name as a slug though is a good idea. I would do it as an optional parameter though so your structure is something like mysite.com/products/{id}/{name} where the url would actually still route correctly without the name. There is an extension method to the string type in the Umbraco.Core namespace called ToUrlSegment() that you can use to clean up the name.
Your new route would be bound like:
RouteTable.Routes.MapUmbracoRoute(
"ProductPage",
"Products/Product/{id}/{slug}",
new
{
controller = "Product",
action = "GetProduct",
slug = UrlParameter.Optional
},
new ProductVirtualNodeRouteHandler()),
new { id = @"(\d)+" }); // Add some validation for the Id.
3.The location of the classes shouldn't really matter as long as they are compiled within the solution and are referenced by namespace correctly. I'm assuming you are using a web application, not a site since you are able to place them in the route.
I would try to structure everything sensibly though and will place my classes in a separate class library project. You could organise yours under a folder called Infrastructure if you want to keep all the code together.
Thanks for the suggestions and feedback. This is what my controller looks like:
public class ProductController : RenderMvcController
{
public ActionResult GetProduct(RenderModel model, int id)
{
var db = new Database("productsDb");
ProductViewModel product =
db.Fetch<ProductViewModel>(@"SELECT p.ProductID, Type, Name, PartNumber, ShortDescription
FROM Products p
WHERE Brand = 'SCT' AND p.ProductID = @id", new {id})
.FirstOrDefault();
return View("Product", product);
}
}
As a side note, I noticed that in the following line:
return View("Product", product);
Product is red like Visual Studio can't find it, I have seen this in other
controllers as well, so not sure if it is a glitch. Just wanted to point it out.
The only warnings are css warnings (3 of them). Not sure if this would help, but initially when I was testing with some static data, I was not inheriting from RenderMvcController and I wasn't getting the null context. Once I inherited from RenderMvcController, that is when I started getting it.
The only thing I can think of that would cause an error like that would be calling the method on a separate thread from the request. It looks like you are not doing that though.
Is it possible to map the Url of child nodes to custom data?
I am connecting to an external database to retrieve products. I have pages for each product, but I am not sure how to get the map the Url to the products returned in the Custom Model. This is what my view currently looks like:
It does inherit from RenderModel, so I do have acccess to the Umbraco extras. I was thinking of creating a table in my external db that would hold the page id's and link them with the products, but this is a last resort since it would require some extra maintenance. Is there another option?
Hi Saied,
What I think you are looking for is a way to map your products to virtual nodes within the CMS yeah?
I.E To be able to list pages for each product without having to create a page for each product in the back office?
If so, you should use an
UmbracoVirtualNodeRouteHandler
to create a custom route for your product pages. There's an excellent article on Shannon's blog about this functionality here but I'll also summarize it with an example of how I'd do it should the URL change.If I'm way off bat here, sorry...
First you would set up a generic document type with matching template in the back office. Lets call it
Product
. A single page of this document type will be used to display each individual product virtually.You then need to create an implementation of
UmbracoVirtualNodeRouteHandler
to tell Umbraco how to map the route to the page.Also create a custom route you can do this via inheriting the
ApplicationEventHandler
class.Now it's time to create a controller. In there you will grab the product from your external database and map it to your view model.
Umbraco should now be able to find all the products by url. You will also now also be able to use
Url.Action()
to generate the correct url to your products since you have created a custom route.Hope that helps
James
P.S I wrote this without the IDE so I dunno if it compiles. It should :)
James,
I have been playing around with the code you gave me and you read my mind. This is exactly what I was looking for. I did implement it and everything worked fine. I am getting an error now, but I also have a few questions:
Product
node have to exist in the back office or is aDocumentType
andTemplate
enough?/products/1
, so I just ended up adding{name}
to my route as well. Do you know if there is a way to avoid having the{id}
parameter in there, but still have access to it? Also,{name}
could have illegal characters in it, so I created a helper to strip those out unless Umbraco has something to handle this?Masterpage
and with a model that does not inherit fromRenderModel
and everything works fine. Now when I include aMasterpage
and inherit fromRenderModel
, when it gets to the following line:var helper = new UmbracoHelper(umbracoContext);
inFindContent
, it throws the following error:Value cannot be null. Parameter name: umbracoContext
Oddly though, have it throws it a few times, it still renders the Product Page, even with an exception.
Thanks again for the great help.
James,
I found this snippet of code and now I don't get the error, but I don't understand what it does and why I don't get the error anymore. I added
umbracoContext = UmbracoContext.EnsureContext(new HttpContextWrapper(HttpContext.Current), ApplicationContext.Current);
to myFindContent
method.Interesting...
Normally that
umbracoContext
parameter shouldn't be null. That method is ensuring that you have anUmbracoHelper
available to query the published content cache for your node and I would much rather we figured out why the code I supplied doesn't work as it should than add the additional code. Did you set up the handlers exactly as I described? Can I see how your controller is coded?To answer your questions though.
Yes. A node of that document type does have to exist in the back office. We need something to pin our virtual product nodes to.
I wouldn't remove the id from your path. There should be a primary key for your product data so that any lookups are fast. Adding the name as a slug though is a good idea. I would do it as an optional parameter though so your structure is something like
mysite.com/products/{id}/{name}
where the url would actually still route correctly without the name. There is an extension method to the string type in theUmbraco.Core
namespace calledToUrlSegment()
that you can use to clean up the name.Your new route would be bound like:
3.The location of the classes shouldn't really matter as long as they are compiled within the solution and are referenced by namespace correctly. I'm assuming you are using a web application, not a site since you are able to place them in the route.
I would try to structure everything sensibly though and will place my classes in a separate class library project. You could organise yours under a folder called Infrastructure if you want to keep all the code together.
Hi James,
Thanks for the suggestions and feedback. This is what my controller looks like:
The error is happening at this line:
The error goes away if I add the following:
As a side note, I noticed that in the following line:
Product
is red like Visual Studio can't find it, I have seen this in other controllers as well, so not sure if it is a glitch. Just wanted to point it out.Thanks Again, Saied
Sorry for the slow reply.
There's nothing I could see in your code that would indicate a problem.
Those lines intrigue me though. Have you tried cleaning and rebuilding the solution. When you build are any warnings shown?
Hi James,
The only warnings are css warnings (3 of them). Not sure if this would help, but initially when I was testing with some static data, I was not inheriting from
RenderMvcController
and I wasn't getting the null context. Once I inherited fromRenderMvcController
, that is when I started getting it.The only thing I can think of that would cause an error like that would be calling the method on a separate thread from the request. It looks like you are not doing that though.
What version of Umbraco are you running?
is working on a reply...