Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Louis Ferreira 69 posts 265 karma points
    Oct 10, 2020 @ 12:41
    Louis Ferreira
    0

    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);
        }
    }
    

    and create a custom view...

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<ProductCatalogue.Core.Models.ProductDto>
    @{
        Layout = "~/Views/master.cshtml";
    }
    
    <h1>hello world</h1>
    

    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.

  • Marc Goodson 2122 posts 14213 karma points MVP 8x c-trib
    Oct 10, 2020 @ 19:31
    Marc Goodson
    0

    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 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:

    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

    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

    Update your view to inherits from

    @using UmbracoViewPage<MyProductViewModel>
    

    regards

    Marc

  • Louis Ferreira 69 posts 265 karma points
    Oct 10, 2020 @ 20:04
    Louis Ferreira
    0

    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 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.

    Does this make sense?

  • Marc Goodson 2122 posts 14213 karma points MVP 8x c-trib
    Oct 11, 2020 @ 07:14
    Marc Goodson
    0

    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:

        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!

    https://github.com/umbraco/Umbraco-CMS/blob/e9627f2ee962a73cd8c007a42845c9129e3339ee/src/Umbraco.Web/Models/ContentModel.cs

    So if you create your 'enriched' ViewModel

    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!)

    regards

    Marc

  • Louis Ferreira 69 posts 265 karma points
    Oct 11, 2020 @ 09:37
    Louis Ferreira
    0

    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:

    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; }
    }
    

    And finally, the View...

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<ProductCatalogue.Core.Web.ViewModels.ProductViewModel>
    @{
        Layout = "~/Views/master.cshtml";
    }
    
    <section class="section">
        <div class="container">
            <div class="row">
                <div class="col-sm-12">
                    <h2>@Model.ProductDto.Name</h2>
                </div>
            </div>
        </div>
    
    </section>
    

    Any suggestions on improvement? Particularly insterested in getting Culture working with this.

    Thanks again for your help so far Marc.

Please Sign in or register to post replies

Write your reply to:

Draft