Need help with Custom Route & IPublishedContent<T>
Hi,
I'm trying to create a custom route to use data from an external source, then render this data on a custom view, but still wanting access to UmbracoContent stuff.
Here is what I have:
Register my custom route...
public class RegisterCustomRouteComposer : ComponentComposer<RegisterCustomRouteComponent>
{ }
public class RegisterCustomRouteComponent : IComponent
{
public void Initialize()
{
RouteTable.Routes.MapRoute(
"ProductCatalogueRoute",
"Products/{action}/{id}",
new
{
controller = "Products",
action = "Index",
id = UrlParameter.Optional
});
}
public void Terminate() { }
}
create a custom controller...
public class ProductsController : Umbraco.Web.Mvc.RenderMvcController
{
private readonly IProductCatalogueClient _service;
public ProductsController(IProductCatalogueClient service)
{
_service = service;
}
public ActionResult Details(int id)
{
ProductDto product = Task.Run(() => _service.GetProductById(id)).Result;
var model = ??? // What to do here so that it is IPublishedContent that has my product as Model?
return View("~/Views/Products/Details.cshtml", model);
}
}
The problem is, how do I create a model that satisfys the view model, and encapsulates my Dto object?
The route to this controller is "/products/details/123". This is correctly intercepted by the controller, but the view is expecting an object of IPublishedContent.
There is a 'special' Umbraco way to map a route to a controller and associated a particular IPublishedContent item with the route, called 'MapUmbracoRoute'
When using MapUmbracoRoute instead of MapRoute, you can associate a 'VirtualNodeRouteHandler' implementation with the route, whose job is to map an IPublishedContent item with the route - usually this is a node in Umbraco.
You create a class that implements IRouteHandler, the easiest way to do this is inherit from the base: UmbracoVirtualNodeRouteHandler
There is an example in the core of implementing a handler that using a hardcoded 'udi' to make the association of IPublishedContent with the route:
(so to experiment you could use this implementation to see how it works, by passing the Udi of the Umbraco node you want to associate with the route)
Your Controller needs to inherits from the special base controller RenderMvcController
and the action of the controller you are mapping the route to should accept a parameter of type (ContentModel model)
This 'model' will be populated with the IPublishedContent item that your RouteHandler decided to associate with the route.
You can then create your own custom view model that inherits another special base Umbraco class: PublishedContentWrapped.
eg
public class MyProductViewModel : PublishedContentWrapped
{
// The PublishedContentWrapped accepts an IPublishedContent item as a constructor
public MyProductViewModel(IPublishedContent content) : base(content) { }
// Custom properties here...
public int StockLevel { get; set; }
public IEnumerable<Distributor> ProductDistributors { get; set; }
}
See how the constructor accepts an IPublishedContent item and because PublishedContentWrapped implements IPublishedContent - now your custom view model does - and you can add whatever additional properties you like to it.
Then in your controller you use the IPublishedContent item from the ContentModel model parameter, to construct your new ViewModel - set your non umbraco properties and pass this to your View
I actually did go down the route of using the MapUmbracoRoute and passing in an instance of a custom class that inherits from VirtualNodeRouteHandler, but I got stuck with the same issue of the constructor requires an instance of IPublishedContent, similar to what you said above. So I don't actually have any instance of IPublishedContent, and that's where my problem lies... if I could create and instance of this somehow, then it would work.
To clarify, my custom route of /products/details/123 is not associated with any content node... the '123' id comes from a custom property editor and the /products/details/ is sort of hard coded.
If you want to map a custom route through Umbraco and 'still have access to UmbracoStuff™' - then Umbraco needs to know the 'context' of the request, there needs to be an associated 'PublishedRequest' assigned to the request. Think about in a view/template you might refer to the current page or look at it's .Children, .Ancestors etc these bits of Umbraco Stuff work from the context of that 'published request'
When a request is made to a page published in Umbraco - then Umbraco takes responsibility for constructing this PublishedRequest, working out from the url, what is the associated published content item, and sending it to the Index action of the default RenderMvcController...
But if you are mapping a 'custom' route, then you have to take responsibility for mapping that PublishedRequest to your custom route, as Umbraco doesn't know how!
You do this by using MapUmbracoRoute and implementing an 'UmbracoVirtualNodeRouteHandler' and attaching it your route.
The job of the 'UmbracoVirtualNodeRouteHandler' is to associate that PublishedRequest to the route:
There is an example in the core of an implemenation doing so here:
My suggestion is to use this, whilst you are getting your head around how this hangs together!
So your mapped route would look something like this:
RouteTable.Routes.MapUmbracoRoute("ProductCustomRoute", "products/{action}/{id}", new
{
controller = "MyProduct",
id = UrlParameter.Optional
}, new UmbracoVirtualNodeByUdiRouteHandler("umb://document/4fed18d8c5e34d5e88cfff3a5b457bf2"));
}
Where the Udi 'umb://document/4fed18d8c5e34d5e88cfff3a5b457bf2' is a reference to an existing Published Content item in Umbraco.
Aside: If I'm building a products section, where the products don't actually exist in Umbraco, but I want to use this technique to pretend that they do, then I'll often, in Umbraco create a 'Products Section' page and use that as the Published Content Request context for my custom route, eg when somebody requests /products/super-widget-1234 the view/template essentially has reference to the 'Products Section' page where I can read all the properties and context from Umbraco - and then my enriched ViewModel has all the properties available of the super-widget-1234 product.
With the route and UmbracoVirtualNodeRouteHandler implementation in place, Umbraco should map request to your MVC Controller.
Your MVC Controller needs to inherit RenderMvcController, and it NEEDS to have in its action signature the ContentModel object...
public class MyProductController : RenderMvcController
{
public ActionResult Details(ContentModel model, string id)
{
In your example above you omit the 'ContentModel model' but this is what your implementation of UmbracoVirtualNodeRouteHandler for the route 'populates' and contains the IPublishedContent item you so crave!
public class MyProductViewModel : PublishedContentWrapped
{
// The PublishedContentWrapped accepts an IPublishedContent item as a constructor
public MyProductViewModel(IPublishedContent content) : base(content) { }
// Custom properties here...
public int StockLevel { get; set; }
public IEnumerable<Distributor> ProductDistributors { get; set; }
}
Then in your controller you can instantiate this custom view model, and pass into it, the IPublishedContent item that your UmbracoVirtualNodeRouteHandler has said should be associated with the request:
eg
public class MyProductController : RenderMvcController
{
public ActionResult Details(ContentModel model, string id)
{
// The IPublishedContent Item is available on the Content property of the model:
var publishedContentItem = model.Content;
// create your viewmodel using it
var productViewModel = new MyProductViewModel(publishedContentItem);
// now get your 'other' product details
var productInfo = myProductServiceFromCustomDb.GetProductById(id);
// populate your viewmodel with your custom properties
productViewModel.StockLevel = productInfo.StockLevel;
//etc
//now return the view/template, passing your enriched viewmodel
Make sure to tell your view about your custom viewmodel:
@using UmbracoViewPage<MyProductViewModel>
Once you have this basic version working, then you could implement your own logic for associating a PublishedContent item to your route in your own custom UmbracoVirtualNodeRouteHandler - to avoid hardcoding an id of a page that might change. Now the thing is here... it doesn't 'have' to be a node in Umbraco, IPublishedContent is just an interface, you can implement that interface however you so choose, and populate your item from outside of Umbraco - it's just monumentally easier to use a node that's published in Umbraco already, and let Umbraco populate all the properties like 'path' and 'level' etc for you.
The trick I sometimes use with an external products/catalogue situation is to have a 'Base Virtual Product' page published in Umbraco, sometimes one for each category, where I store default values, if the 'external' thing doesn't have a particular field - or properties I want editors to be able to update the text for, 'terms and conditions' or a generic category 'introduction' etc etc - the UmbracoVirtualNodeRouteHandler finds the 'category' base virtual product based on the request url...
Hope that's a bit clearer? (it is like magic when it works!)
Your explaination above is pretty good, and I got my head wrapped round most of this out of shear trial and error :) and it confirms that what you said validates my learning curve...
And you'll be glad to know that I came to the same conclusion you mentioned above. I did try and code my our own implementation of IPublishedContent, and then I saw all the interface fields, I was like.... "uhhh, nope!"
Anyway... long story short... I got it working by using the site root nodes' PublishedContent and passing that into the UmbracoVirtualNodeRouteHandler... not ideal, but better than making my own. My only concern now is how will I figure out which Culture the request is coming in on (currently the root node is for a particular culture). I might adopt your idea of a 'Products' node instead of the home page though... might be easier actually.
And yes, your guess as to what I'm trying to do is spot on! I'm toying with creating an external Product Catalogue system that I can hook into Umbraco. I've done all the property editors, PropertyValueConvertors, App Section/Dashboards, etc.... and this is the last piece. Once I got all this firgured out and polished, I may create a YT series on it (Similar to what Paul Seal did)... a sort of 'Deep Dive' series ;)
Anyway, for completeness of this thread, here is what I got so far:
public class RegisterCustomRouteComposer : ComponentComposer<RegisterCustomRouteComponent>
{ }
public class RegisterCustomRouteComponent : IComponent
{
private readonly IUmbracoContextFactory _context;
public RegisterCustomRouteComponent(IUmbracoContextFactory context)
{
_context = context;
}
public void Initialize()
{
using (var cref = _context.EnsureUmbracoContext())
{
var umbracoHelper = cref.UmbracoContext.Content;
var root = umbracoHelper
.GetAtRoot()
.DescendantsOrSelfOfType("home", Thread.CurrentThread.CurrentCulture.Name)
.FirstOrDefault();
RouteTable.Routes.MapUmbracoRoute(
"ProductCatalogueRoute",
"products/{action}/{id}",
new
{
controller = "Products",
id = UrlParameter.Optional
}, new ProductsRouteHandler(root));
}
}
public void Terminate() { }
}
public class ProductsRouteHandler : UmbracoVirtualNodeRouteHandler
{
private readonly IPublishedContent _root;
private IProductCatalogueClient _service => Current.Factory.GetInstance<IProductCatalogueClient>();
public ProductsRouteHandler(IPublishedContent root)
{
_root = root;
}
protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
{
var id = requestContext.RouteData.GetRequiredString("id");
var productDto = Task.Run(() => _service.GetProductById(int.Parse(id))).Result;
if (productDto == null) return null;
var productModel = new ProductViewModel(_root)
{
ProductDto = productDto
};
return productModel;
}
}
public class ProductsController : Umbraco.Web.Mvc.RenderMvcController
{
public ActionResult Details(ContentModel model)
{
return View("~/Views/Products/Details.cshtml", model);
}
}
public class ProductViewModel : PublishedContentWrapped
{
public ProductViewModel(IPublishedContent content) : base(content) { }
public ProductDto ProductDto { get; set; }
}
Need help with Custom Route & IPublishedContent<T>
Hi,
I'm trying to create a custom route to use data from an external source, then render this data on a custom view, but still wanting access to UmbracoContent stuff.
Here is what I have:
Register my custom route...
create a custom controller...
and create a custom view...
The problem is, how do I create a model that satisfys the view model, and encapsulates my Dto object?
The route to this controller is "/products/details/123". This is correctly intercepted by the controller, but the view is expecting an object of IPublishedContent.
I'm stumped :/
Any help is appreciated.
Hi Louis
There is a 'special' Umbraco way to map a route to a controller and associated a particular IPublishedContent item with the route, called 'MapUmbracoRoute'
https://our.umbraco.com/documentation/reference/routing/custom-routes
When using
MapUmbracoRoute
instead ofMapRoute
, you can associate a 'VirtualNodeRouteHandler' implementation with the route, whose job is to map anIPublishedContent
item with the route - usually this is a node in Umbraco.You create a class that implements
IRouteHandler
, the easiest way to do this is inherit from the base:UmbracoVirtualNodeRouteHandler
There is an example in the core of implementing a handler that using a hardcoded 'udi' to make the association of
IPublishedContent
with the route:https://github.com/umbraco/Umbraco-CMS/blob/4be1c3b4c4f26bd0925a3164d32f554b679045da/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByUdiRouteHandler.cs
(so to experiment you could use this implementation to see how it works, by passing the Udi of the Umbraco node you want to associate with the route)
Your Controller needs to inherits from the special base controller
RenderMvcController
and the action of the controller you are mapping the route to should accept a parameter of type (
ContentModel model
)This 'model' will be populated with the IPublishedContent item that your RouteHandler decided to associate with the route.
You can then create your own custom view model that inherits another special base Umbraco class:
PublishedContentWrapped.
eg
See how the constructor accepts an IPublishedContent item and because PublishedContentWrapped implements IPublishedContent - now your custom view model does - and you can add whatever additional properties you like to it.
Then in your controller you use the IPublishedContent item from the ContentModel model parameter, to construct your new ViewModel - set your non umbraco properties and pass this to your View
Update your view to inherits from
regards
Marc
Hi Marc,
Thanks for detailed reply.
I actually did go down the route of using the
MapUmbracoRoute
and passing in an instance of a custom class that inherits fromVirtualNodeRouteHandler
, but I got stuck with the same issue of the constructor requires an instance ofIPublishedContent
, similar to what you said above. So I don't actually have any instance ofIPublishedContent
, and that's where my problem lies... if I could create and instance of this somehow, then it would work.To clarify, my custom route of /products/details/123 is not associated with any content node... the '123' id comes from a custom property editor and the /products/details/ is sort of hard coded.
Does this make sense?
HI Louis
It depends on what you are trying to do.
If you want to map a custom route through Umbraco and 'still have access to UmbracoStuff™' - then Umbraco needs to know the 'context' of the request, there needs to be an associated 'PublishedRequest' assigned to the request. Think about in a view/template you might refer to the current page or look at it's .Children, .Ancestors etc these bits of Umbraco Stuff work from the context of that 'published request'
When a request is made to a page published in Umbraco - then Umbraco takes responsibility for constructing this PublishedRequest, working out from the url, what is the associated published content item, and sending it to the Index action of the default RenderMvcController...
But if you are mapping a 'custom' route, then you have to take responsibility for mapping that PublishedRequest to your custom route, as Umbraco doesn't know how!
You do this by using MapUmbracoRoute and implementing an 'UmbracoVirtualNodeRouteHandler' and attaching it your route.
The job of the 'UmbracoVirtualNodeRouteHandler' is to associate that PublishedRequest to the route:
There is an example in the core of an implemenation doing so here:
https://github.com/umbraco/Umbraco-CMS/blob/4be1c3b4c4f26bd0925a3164d32f554b679045da/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByUdiRouteHandler.cs
My suggestion is to use this, whilst you are getting your head around how this hangs together!
So your mapped route would look something like this:
Where the Udi 'umb://document/4fed18d8c5e34d5e88cfff3a5b457bf2' is a reference to an existing Published Content item in Umbraco.
Aside: If I'm building a products section, where the products don't actually exist in Umbraco, but I want to use this technique to pretend that they do, then I'll often, in Umbraco create a 'Products Section' page and use that as the Published Content Request context for my custom route, eg when somebody requests /products/super-widget-1234 the view/template essentially has reference to the 'Products Section' page where I can read all the properties and context from Umbraco - and then my enriched ViewModel has all the properties available of the super-widget-1234 product.
With the route and UmbracoVirtualNodeRouteHandler implementation in place, Umbraco should map request to your MVC Controller.
Your MVC Controller needs to inherit RenderMvcController, and it NEEDS to have in its action signature the ContentModel object...
In your example above you omit the 'ContentModel model' but this is what your implementation of UmbracoVirtualNodeRouteHandler for the route 'populates' and contains the IPublishedContent item you so crave!
https://github.com/umbraco/Umbraco-CMS/blob/e9627f2ee962a73cd8c007a42845c9129e3339ee/src/Umbraco.Web/Models/ContentModel.cs
So if you create your 'enriched' ViewModel
Then in your controller you can instantiate this custom view model, and pass into it, the IPublishedContent item that your UmbracoVirtualNodeRouteHandler has said should be associated with the request:
eg
Make sure to tell your view about your custom viewmodel:
Once you have this basic version working, then you could implement your own logic for associating a PublishedContent item to your route in your own custom UmbracoVirtualNodeRouteHandler - to avoid hardcoding an id of a page that might change. Now the thing is here... it doesn't 'have' to be a node in Umbraco, IPublishedContent is just an interface, you can implement that interface however you so choose, and populate your item from outside of Umbraco - it's just monumentally easier to use a node that's published in Umbraco already, and let Umbraco populate all the properties like 'path' and 'level' etc for you.
The trick I sometimes use with an external products/catalogue situation is to have a 'Base Virtual Product' page published in Umbraco, sometimes one for each category, where I store default values, if the 'external' thing doesn't have a particular field - or properties I want editors to be able to update the text for, 'terms and conditions' or a generic category 'introduction' etc etc - the UmbracoVirtualNodeRouteHandler finds the 'category' base virtual product based on the request url...
Hope that's a bit clearer? (it is like magic when it works!)
regards
Marc
Hi Marc,
As usual, you answer is as informative as ever!
Your explaination above is pretty good, and I got my head wrapped round most of this out of shear trial and error :) and it confirms that what you said validates my learning curve...
And you'll be glad to know that I came to the same conclusion you mentioned above. I did try and code my our own implementation of
IPublishedContent
, and then I saw all the interface fields, I was like.... "uhhh, nope!"Anyway... long story short... I got it working by using the site root nodes' PublishedContent and passing that into the
UmbracoVirtualNodeRouteHandler
... not ideal, but better than making my own. My only concern now is how will I figure out which Culture the request is coming in on (currently the root node is for a particular culture). I might adopt your idea of a 'Products' node instead of the home page though... might be easier actually.And yes, your guess as to what I'm trying to do is spot on! I'm toying with creating an external Product Catalogue system that I can hook into Umbraco. I've done all the property editors, PropertyValueConvertors, App Section/Dashboards, etc.... and this is the last piece. Once I got all this firgured out and polished, I may create a YT series on it (Similar to what Paul Seal did)... a sort of 'Deep Dive' series ;)
Anyway, for completeness of this thread, here is what I got so far:
And finally, the View...
Any suggestions on improvement? Particularly insterested in getting Culture working with this.
Thanks again for your help so far Marc.
is working on a reply...