Copied to clipboard

Flag this post as spam?

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


  • Ulrik Nedergaard 44 posts 175 karma points
    May 08, 2021 @ 15:05
    Ulrik Nedergaard
    0

    Custom route / MVC

    I'm developing a webshop solution which will run multiple shops out of the same install / IIS instance. It will be small merchandise shops for customers ( shop.customer1.com, customer2shop.com, webshop.customer3.com etc, created in different root nodes with each one having its own products etc).

    So far so good, but I'm struggeling how to figure out to make the cart, terms, contact page etc as a shared thing, that I don't need to manually create under each site.

    I kind of have it working so each site will react to a MVC route called "/cart", but the page doesn't "know" that it belongs under a given site, so master template and navigation etc doesn't work properly, since it has no clue which site the visitor is seen the cart MVC under. Hope it makes sense.

    I suppose I need to do something to make the cart a virtual node, but can't figure out how to make it work.

    https://our.umbraco.com/documentation/implementation/Custom-Routing/

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    May 09, 2021 @ 07:38
    Marc Goodson
    0

    HI Ulrik

    There are a couple of options, but if you have your toe down the custom route path, then the 'bit' where you need to do the magic in the routing example:

       RouteTable.Routes.MapUmbracoRoute(
            "test", 
            "Products/{action}/{sku}",
            new
            {
                controller = "MyProduct",
                sku = UrlParameter.Optional
            },
            new UmbracoVirtualNodeByIdRouteHandler(1234));
        }
    

    Is the 'VirtualNodeRouteHandler' bit: new UmbracoVirtualNodeByIdRouteHandler(1234));

    What this saying is, when this route is requested, Umbraco NEEDS to know which IPublishedContent item /node should be associated with the request, and it's this IPublishedContent item that would give you the context of the site you are 'on' and therefore enable your navigation to work!

    It's called 'virtual' because the IPublishedContent item doesn' need to be a content item in Umbraco, just anything that implements the interface IPublishedContent (there is a base class called PublishedContentWrapped - that makes it easy)

    Anyway, in the example all the UmbracoVirtualNodeByIdRouteHandler is doing is using the Id of a ContentItem to be the 'context' for that request.

    But you may need to do some more complex logic to associate an IPublishedContent item with your request - and if so you can create yoru own VirtualNodeRouteHandler! (bit of a mouthful).

    There is an example here: https://our.umbraco.com/Documentation/Reference/Routing/custom-routes

    Essentially, I'd suggest if you are playing with this first to create a Handler that will return the 'homepage' of your site as the context for the request.

    so if you create a new class called something like

    HomePageRouteHandler

    and make it inherit from

    UmbracoVirtualNodeRouteHandler

    and override

       protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
                {
                    return 'an IPublishedContent item'!!!
                }
    

    In there you have access to RequestContext and UmbracoContext So you can read the requested Url, and then use the UmbracoContext to find the homepage node that matches the requested Url (using the DomainService etc)

    Then when a request comes in for /cart, this code will associate the 'homepage' with it, and your 'Model' will contain properties of the homepage which will allow your navigation to be built...


    Other simpler options would include creating a Cart Document Type, and creating a Cart page under each site, and then use Route Hijackng to create a custom MVC controller which automatically handles all requests to pages based on the Cart DocumentType - then you will be using Umbraco to automatically determine which site you are on, but still be routed to a single MVC controller to do your Cart Business.

    https://our.umbraco.com/Documentation/Reference/Routing/custom-controllers

    Hope that helps give you a steer!

    regards

    Marc

  • Ulrik Nedergaard 44 posts 175 karma points
    May 12, 2021 @ 06:58
    Ulrik Nedergaard
    0

    Thank you for the thorough reply Marc.

    I'm on my way down the Routing-road, and expected that this hardcoded test ( which of course won't work with multiple sites ) - would give me success - but it's not.

    1075 is the id of the root node of the site I'm testing on. I can call /cart and it gives me an error at all code where it expects to find the umbraco stuff. My first error is trying to find the Model.Root.

    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;
    
                RouteTable.Routes.MapUmbracoRoute("Cart", "cart", new
                {
                    controller = "Cart",
                    action = "Index"
                }, new UmbracoVirtualNodeByIdRouteHandler(1075));
    
    
            }
    
        }
    
        public void Terminate()
        {
            // Nothing to terminate
        }
    
    }
    

    Doing manual cart nodes is an option I would prefer not to use, since I would like to be able to expand it with a controller for other kinds of pages in the future without the need for manual creation. :)

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    May 12, 2021 @ 08:28
    Marc Goodson
    0

    Hi Ulrik

    What does your MVC controller look like?

    Does it inherit from RenderMvcController?

    If you put a break point on it, is the routed request going via it, and is the ContentModel populated with your 1075 content?

    public class CartController : Umbraco.Web.Mvc.RenderMvcController
    {
        public override ActionResult Index(ContentModel model)
        {
    

    (the VirtualNodeRouteHandler effectively populates the IPublishedContent property of the ContentModel here)

    In favour of having a 'Cart node' doc type + Route Hijacking approach, it would enable you to have certain 'cart settings' available for editors in Umbraco to change behaviour between different sites.

    If your DocType is called CartPage, then creating a RenderMvcController following the convention of the matching the doc type alias:

       public class CartPageController : Umbraco.Web.Mvc.RenderMvcController
        {
            public override ActionResult Index(ContentModel model)
            {
    

    Then any request to any page based on the CartPage doc type would go via this controller... so 'adding a Cart to a site' is just a case of creating the Umbraco node of that type, the page is immediately routed to the controller, and you immediately have the context of which site you are on...

    ... so it might be more flexible approach that you think.

    You can also have your own BaseRenderMvcController that sits between the core RenderMvcController and any custom route hijacked controller, to share implementation between different types of pages...

    ... anyway just a thought! but be good to get the VirtualNodeRouteHandling approach working for you anyway!

    regards

    Marc

  • Ulrik Nedergaard 44 posts 175 karma points
    Jul 02, 2021 @ 10:02
    Ulrik Nedergaard
    0

    Hi Marc

    Sorry for not getting back you to until now. I got busy doing other things at work, but now I'm back on the project.

    I put together some code and hope for it to work, and it takes me some of the way. I can't say I'm aware of what all the parts does, since it's put together from different stuff found in forums.

    I hoped it would create a route for each site ( doctype : shop ) but if I calling /non-umbraco-page from any site, it will always route to the same shop ( the first i suppose.

    • siteA.com/non-umbraco-page
    • siteB.com/non-umbraco-page
    • siteC.com/non-umbraco-page
    • siteD.com/non-umbraco-page

    .... all routes to siteA.com/non-umbraco-page

    public class RegisterCustomRouteComposer : ComponentComposer

    public class RegisterCustomRouteComponent : IComponent
    {
        private readonly IUmbracoContextFactory _context;
    
        public RegisterCustomRouteComponent(IUmbracoContextFactory context)
        {
            _context = context;
        }
    
        public void Initialize()
        {
            int i = 0;
    
            using (var cref = _context.EnsureUmbracoContext())
            {
                var umbracoHelper = cref.UmbracoContext.Content;
                var shopList = umbracoHelper.GetAtRoot().DescendantsOrSelfOfType("shop");
    
                foreach (var root in shopList)
                {
    
                    i++;
    
                    RouteTable.Routes.MapUmbracoRoute("TestRoute-" + i, "non-umbraco-page", new
                    {
                        controller = "NonUmbracoPage",
                        action = "Index"
                    }, new TestHandler(root));
    
    
                }
            }
    
        }
    
        public void Terminate()
        {
            // Nothing to terminate
        }
    
    }
    
    public class TestHandler : UmbracoVirtualNodeRouteHandler
    {
        private readonly IPublishedContent _node;
    
        public TestHandler(IPublishedContent node)
        {
            _node = node;
        }
    
        protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
        {
            return _node;
        }
    
    }
    
    public class NonUmbracoPageController : RenderMvcController
    {
        // GET
        public ActionResult Index(ContentModel model)
        {
             return View("~/Views/NonUmbracoPage.cshtml",model);
        }
    }
    
  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Jul 03, 2021 @ 10:23
    Marc Goodson
    100

    Hi Ulrik

    Yes, it looks like your custom routes that you are creating aren't taking into consideration the domain...

    eg the multiple routes are all targeting the same relative path

    RouteTable.Routes.MapUmbracoRoute("TestRoute-" + i, "non-umbraco-page", new

    so whichever is the first one written out will be the one matched for all domain requests!

    I'm still in favour here of using Umbraco's built in routing 'as it just works' - particularly if you have multiple domains!

    There are two options that you could quickly get working (and then discount if you don't like it!) using RouteHijacking...

    If your top level document type is called 'shop'

    Create a controller called shopcontroller and make it inherit from RenderMvcController with an Index action like so

    public class ShopController : Umbraco.Web.Mvc.RenderMvcController
    {
        public override ActionResult Index(ContentModel model)
        {
               return CurrentTemplate(model);
        }
    }
    

    Now all requests to a shop 'automatically' are routed via this controller...

    You can then create a 'Cart' Template (not doc type) and allow the shop doctype to use this template.

    You can then expand your controller to match the name of the template you've created eg:

    public class ShopController : Umbraco.Web.Mvc.RenderMvcController
    {
         //handles shop requests
        public override ActionResult Index(ContentModel model)
        {
               return CurrentTemplate(model);
        }
       //handles cart requests
        public ActionResult Cart(ContentModel model)
        {
               // do cart stuff
               return CurrentTemplate(model);
        }
    
    }
    

    So any request to your shop page 'forced' to use the Cart template will be autorouted via the second option.

    siteA.com/cart SiteB.com/cart

    will all be routed via the Cart ActionResult

    (this is because siteA.com/cart is conventionally a shorthand in umbraco for SiteA.com?altTemplate=Cart)

    With this approach you aren't having to think about the routing of the incoming request - Umbraco is 'doing that bit' for you, and you aren't having to have multiple nodes in Umbraco for each site...

    ... that said ...

    I would still create a Cart node underneath each site and use the routehijacking of requests to that Cart type:

    public class CartController : Umbraco.Web.Mvc.RenderMvcController { //handles shop requests public override ActionResult Index(ContentModel model) { return CurrentTemplate(model); } }

    Just because it's easier to see what is going on, and as I said before, in case you want to have certain 'cart settings' etc editable via Umbraco that would be different for each shop!

    regards

    marc

  • Ulrik Nedergaard 44 posts 175 karma points
    Jul 03, 2021 @ 12:42
    Ulrik Nedergaard
    0

    Hi Marc

    Thanks - your solution seems perfect to me. Clean and simple. Having a cart node won't be necessary since I'll just add something to the Shop-node if I should end up with the need for individual cart settings :)

    However I can't make it work.

    To do a test I added "views/test.cshtml" and this code

    public class ShopController : Umbraco.Web.Mvc.RenderMvcController
    {
    
    
        public override ActionResult Index(ContentModel model)
        {
            return CurrentTemplate(model);
        }
    
        public ActionResult Test(ContentModel model)
        {
            return CurrentTemplate(model);
        }
    
    }
    

    But it just gives me a 404.

    Calling "/?altTemplate=Test" also just returns the default Shop template and not the Test.

    I googled it and realized that url/template-rewriting might not be working out of the box in v8.

    So I tried adding this with no success :

    [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
    public class ContentFinderComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            //clear the collection
            composition.WithCollectionBuilder<ContentFinderCollectionBuilder>().Clear();
    
            //add them back in the right order
            composition.WithCollectionBuilder<ContentFinderCollectionBuilder>()
                .Append<ContentFinderByPageIdQuery>()
                .Append<ContentFinderByUrl>()
                .Append<ContentFinderByIdPath>()
    
                //add the url and template one back in here
                .Append<ContentFinderByUrlAndTemplate>()
    
                .Append<ContentFinderByUrlAlias>()
                .Append<ContentFinderByRedirectUrl>();
        }
    }
    

    What am I missing?

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Jul 03, 2021 @ 12:50
    Marc Goodson
    0

    Create the template 'test' in the backoffice... Just having the file on disk isn't enough to enable the routing convention...

    ...and allow the shop doc type to use the test template...

    Ability to route by alt template is enabled by default...

    Setting is in routing section of config/umbraco settings. Config file...

  • Ulrik Nedergaard 44 posts 175 karma points
    Jul 03, 2021 @ 13:37
    Ulrik Nedergaard
    0

    And BOOOOM! It works :) Thank you very much Marc. You're a life saver.

    I found the settings in umbracosettings.config - but it only works if I add the ContentFinderComposer above.

    I suppose having disableAlternativeTemplates set to false should make it work, but it just gives me 404.

    <web.routing trySkipIisCustomErrors="true" internalRedirectPreservesTemplate="false" disableAlternativeTemplates="false" validateAlternativeTemplates="false" disableFindContentByIdPath="false" umbracoApplicationUrl="">
    

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Jul 03, 2021 @ 15:17
    Marc Goodson
    0

    Hi Ulrik

    Yes, think you are right in V7 the content finder is registered by default, but in V8 they took it out... would probably make sense if the setting controlled that :-P

    But glad you got something working!

    regards

    Marc

Please Sign in or register to post replies

Write your reply to:

Draft