Copied to clipboard

Flag this post as spam?

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


  • Keith Jackson 183 posts 552 karma points
    Feb 02, 2013 @ 23:21
    Keith Jackson
    0

    Umbraco 4.11 - MVC Custom Routing - Content is null - How can I load content

    I've got a routing setup for a blog within my site that works like this...

    • siteroot/blog
    • siteroot/blog/page2
    • siteroot/blog/page3

    etc. etc.

    So I set up the following cutom routes...

                routes.MapRoute(
    null, "blog/page",
    new { controller = "blog", action = "index" });

    routes.MapRoute(
    null, "blog/page{page}",
    new { controller = "blog", action = "showpage", page = UrlParameter.Optional },
    new { page = @"\d+" });

    routes.MapRoute(
    null, "blog/rss.xml",
    new { controller = "blog", action = "feed" });

    All is well and good, but when the showpage action on the BlogController is hit the value of the model is null.

        public class BlogController : RenderMvcController
    {
    /// <summary>
    /// Default method on the controller.
    /// </summary>
    /// <param name="model">The model.</param>
    /// <returns>The first page of the blog.</returns>
    public override ActionResult Index(RenderModel model)
    {
    return ShowPage(model);
    }

    /// <summary>
    /// Shows the page.
    /// </summary>
    /// <param name="model">The model.</param>
    /// <param name="page">The page.</param>
    /// <returns></returns>
    public ActionResult ShowPage(RenderModel model, int page = 1)
    {
    var blogRoll = (model == null) ? NullBlogRoll.Value : new BlogRoll(model.Content);

    // This is null so will need to enforsce usage of the blog model rather than the content.

    return View("Blog", new BlogListViewModel(blogRoll) { CurrentPage = page });
    }
    }

    This is kind of expected as Umbraco seems to be looking for some content called 'page2' or 'page3' (for example). What I need to be able to do is to either....

    1. Somehow return the 'parent' intem (the blog root) as 'model'
    2. Load the blog root manually in the event the model value is null.

    The problem I have here is that in the controller I have no UmbracoHelper or UmbracoContext with which to load content data.

    I ghave a nice wrapper that is on a base view for all of my views that contains the Umbraco Helper methods, wrapped up to load specific content items, but it's too far down the chain. As a workaround I've had to but some skanky logic on a wrapping model where you can query if the underlying model is null...

        public class BlogListViewModel : MinistrywebBaseViewModel<IBlogRoll>
    {
    #region | Construction |

    /// <summary>
    /// Initializes a new instance of the <see cref="BlogListViewModel" /> class.
    /// </summary>
    /// <param name="blog">The blog.</param>
    public BlogListViewModel(IBlogRoll blog)
    {
    InnerObject = blog;
    }

    #endregion

    /// <summary>
    /// Gets or sets the blog roll.
    /// </summary>
    public IBlogRoll BlogRoll
    {
    get { return InnerObject; }
    set { InnerObject = value; }
    }

    /// <summary>
    /// Gets a value indicating whether this instance has null blog roll.
    /// </summary>
    public bool HasNullBlogRoll { get { return InnerObject == NullBlogRoll.Value; } }

    public int CurrentPage { get; set; }

    public IEnumerable<Article> ArticleSummaries
    {
    get
    {
    var articleRemovalQuotient = InnerObject.ArticlesPerPage * (CurrentPage - 1);
    return InnerObject.Articles.Skip(articleRemovalQuotient).Take(InnerObject.ArticlesPerPage);
    }
    }

     

    This is however, kind of sucky and seriously messes up any kind of seperation of concerns. (It looks like this in the view)...

    @inherits MinistrywebViewPage<BlogListViewModel>
    @{
    Layout = "../_Layout.cshtml";

    // Because of the routes used, the blog content model may be null.
    if (Model.HasNullBlogRoll)
    {
    Model.BlogRoll = Ministryweb.BlogRoll;
    }

    ViewBag.Title = Model.Name + " - " + Ministryweb.RootAncestorName;
    }

    (shivers involuntarilly)

    Is there some code I can add to the controller to get ahold of the Umbraco context and return the content there? If I can do that then I don't need the model content to be passed into the controller, as I know what the ID is and it never changes.

  • Sebastiaan Janssen 5045 posts 15476 karma points MVP admin hq
    Feb 03, 2013 @ 13:37
    Sebastiaan Janssen
    0

    I think you might be doing things the wrong way around. 

    The easiest way to work with this in Umbraco is to let Umbraco controll your routes, you can then create the blog/rss/etc in the content tree and the only thing you need to do is query the content in your view.

    Then when you need some custom functionality like a form, you can use something called a SurfaceController (inherits from Controller but gives you some additional Umbraco access easily).

  • Keith Jackson 183 posts 552 karma points
    Feb 03, 2013 @ 14:41
    Keith Jackson
    0

    All of the rites utilise the content that is under the /blog node, so this doesn't work.

     

    The RSS feed could probably be rewritten to use an alt. Template approach which would probably be better, and would nicely route in the standard way but this doesn't sort out the page routes - these determine which page of the blog is shown. This is s lust of X number of the child content items (articles), with a different set shown dependent on the value of the page parameter.

    You can see an example of this working on Umbraco 5 at www.ministryotech.co.uk/blog/page2 for example.

  • Phill 115 posts 288 karma points
    Feb 10, 2013 @ 18:10
    Phill
    0

    Hi Sebastian, I'm having a similar issue as per this thread http://our.umbraco.org/forum/core/general/37617-Is-it-possible-to-use-parameters-from-the-URL-in-MVC-Actions  You say Kieth is doing things the wrong way around but you avoid suggesting a solution to the core issue he's trying to resolve. Yes he could add blog/rss/etc to the content tree but that's the easy part. What is the recommended way for handling a customcontroller and content for data that doesn't exist in the content tree? I've made a bit of progress and have a custom route that I'm able to get my custom controller to fire, and I'm able to get the required action and ID that's passed via the url, but when I try to do something simple like return base.Index(model) the RenderModel "model" is always null.

    If this is indeed the wrong approach for mapping /blog/page1, /blog/page2 (when page1 and page2 aren't in tree) or in my case /gallery/photos1, /gallery/photos2, then what is the right approach? Obviously creating all those pages as pages in the content tree doesn't make much sense. There has to be an easy solution for this but I just can't seem to find it anywhere.

    Thanks for your help.

    Phill

  • Sebastiaan Janssen 5045 posts 15476 karma points MVP admin hq
    Feb 10, 2013 @ 18:14
    Sebastiaan Janssen
    0

    You should probably check out hijacking routes. http://our.umbraco.org/Documentation/Reference/Mvc/custom-controllers

  • Sebastiaan Janssen 5045 posts 15476 karma points MVP admin hq
    Feb 10, 2013 @ 18:16
    Sebastiaan Janssen
    0

    Ah ok, you already did that. You don't need to create a route, the idea is that you hijack the original route from Umbraco and then it knows the context.

  • Phill 115 posts 288 karma points
    Feb 10, 2013 @ 18:19
    Phill
    0

    Hi Sebastiaan, thank's for the pointers but if I just follow the hijacking route insructions I have no access to the extra info in the url I need (in this case {id} or {galleryid}), if I add that to the url then I get an umbraco 404 error and the hijack code is never hit. The only way I can around the 404 error issue is to add a route mapping. Any other suggestions?

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Feb 10, 2013 @ 18:34
    Shannon Deminick
    0

    I'll get a working version of this up and running tomorrow and update the documentation (and of course reply to this thread :)

  • Phill 115 posts 288 karma points
    Feb 10, 2013 @ 18:40
    Phill
    0

    Thanks Shannon, looking forward to it! Even though it might be embarassing, hopefully it's something relatively simple!

    Phill

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Feb 10, 2013 @ 18:42
    Shannon Deminick
    0

    It'll be an interesting excercise and will probably expose some required API features to make it easier in the future.

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Feb 12, 2013 @ 01:45
    Shannon Deminick
    0

    Really sorry i didn't get around to this today, had a ton of other work but promise to do it tomorrow!!

  • Phill 115 posts 288 karma points
    Feb 12, 2013 @ 22:15
    Phill
    0

    Hey Shannon, if you don't have time for a complete example, is there any way you can spare 5 min to point me in the right direction? Thanks!

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Feb 13, 2013 @ 01:23
    Shannon Deminick
    0

    Hey Phil, just working on this now. I just noticed one of your comments saying that we don't expose an UmbracoHelper or anything on RenderMvcController... what version are you using. I can assure that we definitely do this in 6.1 (which I'm working in now) and I'm pretty sure we also expose it there on later version of 4.x too as well as the Services (ServiceContext), etc... You can easily create your own UmbracoHelper in your controller in the mean time:

    new UmbracoHelper(UmbracoContext)

    ... I'm positive that we expose the UmbracoContext even in older 4.x version that have MVC in RenderMvcController. And just in case we don't you can use the singleton:

    new UmbracoHelper(UmbracoContext.Current)

    That might help you a bit but doing the routing properly/nicely is a different ballgame, i'll report back real soon!

  • Phill 115 posts 288 karma points
    Feb 13, 2013 @ 01:58
    Phill
    0

    Hi Shannon, That was Keith that commented about lack of access to Umbraco Helper so I can't comment on what his setup is. I just hijacked his thread a bit since I'm having a very similar issue and trying to accomplish basically the same result. I'm running 4.11.4, I tried v6 very briefly but encountered a bug rather quickly so decided to play it safe and stick with v 4.xx for now.

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Feb 13, 2013 @ 02:56
    Shannon Deminick
    3

    Ok I have two ways to get this job done so you can choose what you like best, there's pros/cons for each. This is a basic setup which requires you to specify the Id of the node that you want to bind for your action however you could extend this to work however you want. I'll think about some different ways we could set this up in the future but I think it's pretty reasonable as is. This code is made for brevity, it is not error checking, etc... it does contain a lot of comments so please read them. There's also more notes to read at the bottom of this post :)

    The first option is to specify a custom route handler

    So first off, let's look at the route defined:

    //Route with a custom handler
    var route = RouteTable.Routes.MapRoute(
            null, "blog/pagewithhandler{page}",
            new { controller = "Home", action = "ShowPageUsingHandler", page = UrlParameter.Optional },
            new { page = @"\d+" });        
    route.RouteHandler = new SpecificPageRouteHandler(1046);

    You'll see that we've specified a custom route handler: SpecificPageRouteHandler and we're passing in a page Id that we want bound in our action (in this case 1046). Next, let's have a look at this custom route handler code:

    public class SpecificPageRouteHandler : IRouteHandler
    {
        public SpecificPageRouteHandler(int pageId)
        {
            _pageId = pageId;
        }
        private readonly int _pageId;
        private UmbracoHelper _umbraco;    
        public UmbracoHelper Umbraco
        {
            get { return _umbraco ?? (_umbraco = new UmbracoHelper(UmbracoContext.Current)); }
        }
        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            //we want to lookup the document by id
            var content = Umbraco.TypedContent(_pageId);
            if (content != null)
            {            
                //Create a RenderModel object and put it in the DataTokens... this
                //is how the RenderModelBinder will bind the model to the property of an action.
                //However, this sets the standard culture, this will not set a specific culture
                //based on a domain, you'd need to write the logic for that (which can be found in
                //the source in the object DomainHelper)
                requestContext.RouteData.DataTokens["umbraco"] = 
                    new RenderModel(content, CultureInfo.CurrentUICulture);
            }
            return new MvcHandler(requestContext);
        }
    }

    Not much code here really, the main thing to focus on is the GetHttpHandler method. It simply uses the UmbracoHelper (which we've created as a property) to lookup a page by the Id specified in the ctor. If it is found we chuck the result into the current route's DataTokens. This is because our standard RenderModelBinder uses this value to bind the object to the Action's parameter. So then let's look at the controller code for this action:

    public class HomeController : Umbraco.Web.Mvc.RenderMvcController
    {
        public ActionResult ShowPageUsingHandler(RenderModel model, int page = 1)
        {
            return Content("WOOT!, model bound: " + model.Content.Id);
        }
    } 

    Not much to look at there but the model parameter is now bound to the document with ID: 1046.

     

    The second option is to specify a custom model binder

     

    First, lets look at the route:

    RouteTable.Routes.MapRoute(
            null, "blog/pagewithbinder{page}",
            new { controller = "Home", action = "ShowPageUsingBinder", page = UrlParameter.Optional },
            new { page = @"\d+" });  

    This is just a normal route, pretty much exactly the route that was first posted here. Next, let's look at the controller/action:

    public class HomeController : Umbraco.Web.Mvc.RenderMvcController
    {
        public ActionResult ShowPageUsingBinder(
            [SpecificPageModelBinder(1046)]RenderModel model, int page = 1)
        {
            return Content("WOOT!, model bound: " + model.Content.Id);
        }
    }

    As you can see this is slightly different. We are specifying a custom model binder directly on the action's model parameter. This will bypass the standard RenderModelBinder and use a custom one. Also notice that we are passing in the Id 1046 to this binder which will ensure that the document 1046 is bound to the model parameter. Let's look at the code for this custom model binder:

    public class SpecificPageModelBinder : CustomModelBinderAttribute, IModelBinder
    {
        private readonly int _id;
        public SpecificPageModelBinder(int id)
        {
            _id = id;
        }
        public override IModelBinder GetBinder()
        {
            return this;
        }
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            //we want to lookup the document by id
            var content = Umbraco.TypedContent(_id);
            if (content != null)
            {
                //Return a new RenderModel.
                //However, this sets the standard culture, this will not set a specific culture
                //based on a domain, you'd need to write the logic for that (which can be found in
                //the source in the object DomainHelper)
                return new RenderModel(content, CultureInfo.CurrentUICulture);                
            }
            return null;
        }
        private UmbracoHelper _umbraco;
        public UmbracoHelper Umbraco
        {
            get { return _umbraco ?? (_umbraco = new UmbracoHelper(UmbracoContext.Current)); }
        }
    }

    The cool part about this model binder is that it is both a model binder attribute and an IModel binder at the same time therefore for the GetBinder method we can just return 'this' and then in the BindModel method we just look up the document by the Id we passed to it's ctor and return a new RenderModel if it is found.

    So the choice is yours. I personally prefer the route handler way just because it's a bit more in-line with how MVC is supposed to work plus the model will be set in the MVC pipeline whereas the model binder way is not. With the route handler way you could still use Action filters and get access to the model during action execution but I don't think it would be possible with the Model binder way since I think the model binder will fire too late in the pipeline.

    I've put lots of notes in the comments which mostly relate to setting the Culture. I've left all of this code out for brevity and currently we don't expose the DomainHelper publicly but if Culture matters to you for this excersise it is still possible you'd just need to see what the DomainHelper does in the codebase. Though you'd have to make sure that the route that you've specified is in the same URL path as a node that you've set culture in order for that to work.

    Let me know if this helps or doesn't help :)

  • Keith Jackson 183 posts 552 karma points
    Feb 13, 2013 @ 11:52
    Keith Jackson
    0

    This is great stuff Shannon - I'm with you, I much prefer the RouteHandler approach, it's really clean and descriptive as to what you are doing, whereas the custom model binder feels a bit more 'hacked in'.

    Sorry about my earlier comments about UmbracoHelper - I only recently found teh contained UmbracoContext on RenderMvcController the other day when I was looking (ironically) at using the new API for U6 (Different off-topic story).

  • Phill 115 posts 288 karma points
    Feb 13, 2013 @ 14:02
    Phill
    0

    Thanks so much for this Shannon. It defintely got me over my hurdle and I now have a nice Gallery List (shows Media Folders) and Gallery View (shows images in selected folder) setup working. 

    As food for thought for future functionality within Umbraco, something I often like to do within a CMS is to have shared content under one folder, usually hidden from menus (i.e. staff profiles) and then through the site I might have different pages that have a list/detail setup so you can see a list of a subset of the shared content and view each one in detail. There can be a list elsewhere in the site on a different page with a different subset of shared content. For my CMS editors (clients) this makes keeping content up to date really easy as they only have to update the shared content in one place. However trying to implement this in the current Umbraco MVC setup with having to add a hard coded Route and ContentID makes impossible as the client would have to come back to me every time they wanted to add a page. How to pull this off is beyond my Umbraco/MVC abilities but I think it could be a useful feature set/functionality. Maybe some how setup as a custom, built in document type that allows for list/detail (with urls) based on content selected with Page Picker?

    Anyway, don't want to take away from you providing this solution as it's saved me a whole lot of work now that I don't have to set up a page for each gallery and select the images for each page!

    Thanks again!

    Phill

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Feb 13, 2013 @ 15:15
    Shannon Deminick
    0

    Hey @Phil, yup was thinking about the same kind of logic whilst making this... still have to think about the right approach to that :)

  • [email protected] 4 posts 24 karma points
    Mar 06, 2013 @ 07:08
    ranganapeiris@gmail.com
    0

    Hey @Phil, Could you please show a code segment on your IRouteHandler implementation and how it is being used in the route. Because it seems the solution given, uses a hard coded page Id (It works with a given id. I checked). But I have few pages from the same document type where all should support the same route with different route values.

    @Shannon, Any other changes to get rid of this hard coded page id and make it generic for all nodes from a given type?

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Mar 06, 2013 @ 15:48
    Shannon Deminick
    0

    @ranganapeiris if you want to route via document type then you should just use route hijacking: http://our.umbraco.org/documentation/Reference/Mvc/custom-controllers

  • [email protected] 4 posts 24 karma points
    Mar 06, 2013 @ 20:27
    ranganapeiris@gmail.com
    0

    Thanks Shannon. But I want to have a custom route like site\{category}\{duration} for document types. There are many doctype objects like that.

     

  • Michael Falk 37 posts 59 karma points
    Apr 07, 2013 @ 16:21
    Michael Falk
    0

    Hi Shannon

    I am trying to use the described method, and apparently everything is working, however, if i in the view i am using, is using

    @Umbraco.Field("pageHeader", altFieldAlias: "pageTitle")

    i get a Nullreference exception:
    [NullReferenceException: Object reference not set to an instance of an object.] Umbraco.Web.UmbracoHelper.Field(IPublishedContent currentPage, String fieldAlias, String altFieldAlias, String altText, String insertBefore, String insertAfter, Boolean recursive, Boolean convertLineBreaks, Boolean removeParagraphTags, RenderFieldCaseType casing, RenderFieldEncodingType encoding, Boolean formatAsDate, Boolean formatAsDateWithTime, String formatAsDateWithTimeSeparator) +1547 Umbraco.Web.UmbracoHelper.Field(String fieldAlias, String altFieldAlias, String altText, String insertBefore, String insertAfter, Boolean recursive, Boolean convertLineBreaks, Boolean removeParagraphTags, RenderFieldCaseType casing, RenderFieldEncodingType encoding, Boolean formatAsDate, Boolean formatAsDateWithTime, String formatAsDateWithTimeSeparator) +104 ASP._Page_Views_BlogLanding_cshtml.Execute() in c:\Development\Umbraco\Falk118\Falk118\Falk118.WebApp\Views\BlogLanding.cshtml:21

    if i use @Model.Content["pageHeader"], then it works fine.

    Do you have any idea why? (I am using 6.0.3)

    br

    Michael

     

  • Michael Falk 37 posts 59 karma points
    Apr 07, 2013 @ 16:41
    Michael Falk
    0

    I got a little further. one issue is that PageId is not set on the UmbracoContext, i can fix that in the SpecificPageRouteHandler like this:

                    UmbracoContext.Current.HttpContext.Items["pageID"] = _pageId;
    

    How the next issue is then:

    Cannot render a macro when there is no current PublishedContentRequest

    And i cannot figure ouot a way to the that.

     

    Help! :-)

    br
    Michael

  • Michael Falk 37 posts 59 karma points
    Apr 07, 2013 @ 18:02
    Michael Falk
    0

    Seems like this would fix it:

    var docRequest = UmbracoContext.Current.PublishedContentRequest;
    requestContext.RouteData.DataTokens.Add("umbraco-doc-request", docRequest); //required for RenderMvcController

    But PublishedContentRequest is Internal :-(

     

  • Jeroen Breuer 4908 posts 12265 karma points MVP 4x admin c-trib
    Apr 08, 2013 @ 09:30
    Jeroen Breuer
    0

    The PublishedContentRequest will probably be public in the 6.1 final: http://issues.umbraco.org/issue/U4-1528

    Jeroen

  • Michael Falk 37 posts 59 karma points
    Apr 08, 2013 @ 11:42
    Michael Falk
    0

    Yes i saw that, but looking in the latest source, it does not appear to have happened yet :-(

    br
    Michael

  • kristian schneider 190 posts 351 karma points
    Apr 30, 2013 @ 00:37
    kristian schneider
    0

    Just as a note.

    I had a similar issue and got stuck with the PublishedContentRequest  not beein public.

    After trying all sorts of stuff I ended up using good 'ol urlrewriting and then handling the paramenter in the razor view

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    Apr 30, 2013 @ 00:40
    Shannon Deminick
    0

    @kristian, can you describe the problem that 'you' are having as there's quite a few different/varying issues and work arounds posted on this thread.

  • kristian schneider 190 posts 351 karma points
    Apr 30, 2013 @ 09:20
    kristian schneider
    0

    I had a problemer similar to Keith Jackson in that I wanted to use parts of the URL as a parameter:

    /Psychologist/name1

    /Psychologist/name2

    I managed to route the requste to a Controller that handled the request and used /Psychologist/ as the page id for the page model. 

    The problem I ran into was I also use a nested layout where the parent template has a macro that renders a menu. When I tries to render the menu I get:

    Cannot render a macro when there is no current PublishedContentRequest

    So thats where I got stuck and ended up using urlrewrite instead.

  • Simon Dingley 1470 posts 3427 karma points c-trib
    Mar 11, 2014 @ 10:54
    Simon Dingley
    0

    Kristian, how do you get around this as I have the same problem in v6.1.6?

  • Logan P. 47 posts 217 karma points
    May 08, 2014 @ 20:09
    Logan P.
    0

    I too am having this same problem in 7.1. This seems like a pretty big oversight considering we are dealing with a MVC framework. I feel like this is basic functionality. There surely has to be some work around for this. Has anyone came up with a solution yet? For now I guess I will have to do url rewrites to get around this. 

    Thanks!

  • Shannon Deminick 1524 posts 5270 karma points MVP 2x
    May 09, 2014 @ 08:47
    Shannon Deminick
    0

    Hi guys,

    Really sorry i haven't had much time to reply!!

    The underlying reason this isn't working (i.e. a custom route like /Psychologist/name1 ) is because Umbraco doesn't know what that means, there is no node associated with that route.

    So you can create your own route for that URL or use URL rewriting (which i know isn't what you are after) or create an IContentFinder.

    Custom routing to then resolve an Umbraco model and wire up everything as if it were in the Umbraco pipeline can be done along these lines: http://shazwazza.com/post/Custom-MVC-routing-in-Umbraco

    Also note that there is a new: EnsurePublishedContentRequestAttribute

    and more of this discussion can be found here: http://our.umbraco.org/forum/developers/extending-umbraco/41367-Umbraco-6-MVC-Custom-MVC-Route?p=3

    An IContentFinder might be your easiest option, but this depends on what you are are trying to achieve. An IContentFinder can match your URL and then assign an IPublishedContent item to the published content request which means that Umbraco will now know about that URL and for what node to assign to it.

    Lastly, I do plan on trying to make this process easier so that we can map URLs/Routes that Umbraco doesn't know about to map them to nodes (like IContentFinder) but do this more along the lines of how you set up MVC routes

Please Sign in or register to post replies

Write your reply to:

Draft