Copied to clipboard

Flag this post as spam?

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


  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 16:19
    Dominic Resch
    0

    Output of a list of objects within a view with specific routes

    Hello everyone.

    What I need (Umbraco 13):

    I would like to display a list of search results when calling the following URLs.

    • /{culture}/discipline/{discipline}
    • /{culture}/disziplin/{discipline}
    • /{culture}/topic/{topic}
    • /{culture}/thema/{topic}

    The URLs are therefore also language-dependent, in this case English and German, but more could be added in the future.

    A distinction is also made between topic and discipline. The resulting list is the same, e.g. List of SearchResult objects. Only the logic within is different.

    The view should have the master.cshtml defined as the layout, which currently contains the following line:

    @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
    

    Means I have to return content somehow.

    I have now tried to define custom routes and have them point to a controller, which has 2 actions, which in turn returns the view with the list of search results. But I always got the error message that the model cannot be bound because it is not IPublishedContent. At least when I used the layout. Without the layout it worked without any problems.

    I didn't create any document types or content nodes either, as I don't think they help me very much here. But none of it really works.

    Has anyone already done something like this? I don't need a full implementation of course, just some guidance. A direction that leads me to the solution. Do I need to create custom routes? Do I have to create document types and solve this via route hijacking? Is there an alternative, perhaps without document types?

    UPDATE

    My current implementation (quick and dirty, since I'm only evaluating Umbraco currently to see if it fits our needs):

    Composer:

    public class SearchResultsControllerComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
            => builder.Services.Configure<UmbracoPipelineOptions>(options =>
            {
                options.AddFilter(new UmbracoPipelineFilter(nameof(SearchResultsController))
                {
                    Endpoints = app => app.UseEndpoints(endpoints =>
                    {
                        // German
                        endpoints.MapControllerRoute(
                                "Search results controller - Disziplinen",
                                "/{culture}/disziplin/{discipline?}",
                                new { Controller = "SearchResults", Action = "Discipline" })
                            .ForUmbracoPage(FindContent);
                        endpoints.MapControllerRoute(
                                "Search results controller - Thema",
                                "/{culture}/thema/{topic?}",
                                new { Controller = "SearchResults", Action = "Topic" })
                            .ForUmbracoPage(FindContent);
    
                        // English
                        endpoints.MapControllerRoute(
                                "Search results controller - Discipline",
                                "/{culture}/discipline/{discipline?}",
                                new { Controller = "SearchResults", Action = "Discipline" })
                            .ForUmbracoPage(FindContent);
                        endpoints.MapControllerRoute(
                                "Search results controller - Topic",
                                "/{culture}/topic/{topic?}",
                                new { Controller = "SearchResults", Action = "Topic" })
                            .ForUmbracoPage(FindContent);
                    })
                });
            });
    
        private IPublishedContent FindContent(ActionExecutingContext actionExecutingContext)
        {
            var umbracoContextAccessor = actionExecutingContext.HttpContext.RequestServices
                .GetRequiredService<IUmbracoContextAccessor>();
    
            IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
            IPublishedContent? root = umbracoContext.Content.GetAtRoot().FirstOrDefault();
    
            return root;
        }
    }
    

    Controller:

    public class SearchResultsController(
        ILogger<UmbracoPageController> logger,
        ICompositeViewEngine compositeViewEngine,
        ISearchService searchService) : UmbracoPageController(logger, compositeViewEngine)
    {
        [HttpGet]
        public IActionResult Discipline(string culture, string discipline)
        {
            IEnumerable<IPublishedContent> content = searchService.SearchContentTag(culture, discipline);
    
            return View(nameof(Index), GetSearchResult(content).ToList());
        }
    
        [HttpGet]
        public IActionResult Index(IEnumerable<SearchResultByTagModel> searchResults)
            => CurrentTemplate(CurrentPage);
    
        [HttpGet]
        public IActionResult Topic(string culture, string topic)
        {
            IEnumerable<IPublishedContent> content = searchService.SearchContentTag(culture, topic);
    
            return View(nameof(Index), GetSearchResult(content).ToList());
        }
    
        private IEnumerable<SearchResultByTagModel> GetSearchResult(IEnumerable<IPublishedContent> pages)
            => pages.Select(x => new SearchResultByTagModel(x)
            {
                Name = x.Name,
                Url = x.Url()
            });
    }
    

    index.cshtml (of my controller):

    @using Project.Umbraco.Models
    @model List<SearchResultByTagModel>
    
    @{
        Layout = "../master.cshtml";
    }
    
    <article class="box">
        @foreach (SearchResultByTagModel item in Model)
        {
    
        }
    </article>
    

    master.cshtml (shortened):

    @using System.Globalization
    
    @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
    @{
        Layout = null;
    
        var home = (Home)Model.Root();
        var font = home.Font;
        var colorTheme = home.ColorTheme;
    
        var currentCultureName = @Model.GetCultureFromDomains().ToLower();
    }
    
    <!DOCTYPE HTML>
    <html lang="@currentCultureName">
        <head>
            <title>@Model.Name - @home.Sitename</title>
        </head>
        <body class="left-sidebar is-preload">
            @RenderBody()
        </body>
    </html>
    
  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 18:34
    pbl_dk
    0

    Can you post your controller functions?

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 18:57
    Dominic Resch
    0

    Updated my initial post

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 19:12
    pbl_dk
    0

    I am not sure if you are passing the model.. there is something in the docs here: https://docs.umbraco.com/umbraco-cms/reference/routing/custom-controllers

    To use a specific custom view model, the @inherits directive will need to be updated to reference your custom model using the Umbraco.Cms.Web.Common.Views.UmbracoViewPage

    @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<MyProductViewModel>
    

    It should be pretty straightforward, I have made a search like it in U13 with a controller and IUmbracoContextAccessor. But I havent gotten so far to output it as a view, since I was using it as a JSON API.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 19:18
    Dominic Resch
    0

    I'm pretty sure I've already tried that. But I'll try it again now, just to be on the safe side. I just have to install .NET 8 (I'm no longer in the office) and will let you know as soon as I know anything.

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 19:20
    pbl_dk
    0

    Yes try to check out those docs, there are many examples of passing models to views. If you dont get any errors here:

        IEnumerable<IPublishedContent> content = searchService.SearchContentTag(culture, topic);
    

    I cant see why the data should not be valid, and there might be some MVC issue more than IPublishedContent issue.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 19:26
    Dominic Resch
    0

    In general, I also assume that there should be no error here. If I don't use my master.cshtml as the layout for my index.cshtml, then it also works.

    But as soon as I use the master.cshtml, I get the error message. I can copy it as soon as I have installed it and tested your suggestion.

    But I don't do anything special in the master.cshtml. And the only reason why I need UmbracoViewPage there is to access the dictionary.

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 19:29
    pbl_dk
    0

    I just found this in my code, I am actually forcing the result to be IPublishedContent here for it to work:

    IEnumerable<IPublishedContent> content = ctx!.Children.OfType<IPublishedContent>();
    
  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 19:32
    Dominic Resch
    0

    Theoretically, I do that too. At least in my controller. The content list is of type IPublishedContent. And I think that if I only pass one of them to the view, it will work. But after I take the IPublishedContent and wrap it in a ViewModel (simple class, only with the properties Name and URL), it is no longer IPublishedContent.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 19:53
    Dominic Resch
    0

    I now have this in my index.cshtml:

    @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<List<SearchResultByTagModel>>
    

    And get the following exception:

    ModelBindingException: Cannot bind source type System.Collections.Generic.List`1[[Project.Umbraco.Models.SearchResultByTagModel, Project.Umbraco, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] to model type Umbraco.Cms.Core.Models.PublishedContent.IPublishedContent.
    

    Same when I create a new class, containing a property with the list and use that instead. Same error (just with the other class).

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 19:55
    pbl_dk
    0

    yeah, I am trying to get it to work, but get a lot of strange errors.. I have to read those docs a while.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 19:57
    Dominic Resch
    0

    Yes, I'm going to continue to dig through the docs too. Thanks for your help! Something else that might be relevant: In the docs you sent, it says below the line you copied:

    <> contains a model generated for each Document Type to give strongly typed access to the Document Type properties in the template view.
    

    But I am not using a Document Type here. So maybe the complete UmbracoPageView is wrong and it has to be done differently.

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 20:01
    pbl_dk
    0

    Yes there is also something here:

    In most cases you will need your custom model to build upon the underlying existing PublishedContent model for the page. This can be achieved by making your custom model inherit from a special base class called PublishedContentWrapped

    PublishedContentWrapped will take care of populating all the usual underlying Umbraco properties and means the @Model. syntax will continue to work in the layouts used by your template.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 20:16
    Dominic Resch
    0

    "Upon the underlying existing PublishedContent". But the problem is that my list (or class) has no underlying PublishedContent.

    I'm afraid that the problem starts before that. If you set such a custom route (as in my Composer), then there is the method "ForUmbracoPage". And the FindContent method should also return a PublishedContent.

    So that already implies that this is actually about content, not random classes. Perhaps the complete creation of the custom routes is a waste of time because I generally don't work with document types here?

    Perhaps I need to define the custom routes differently after all?

    And for your information: "search" is actually the wrong word here. I have a search that also works, with AJAX and calling an API. This is just about "navigation". I simply want to list all content nodes of a document type that have a certain tag (either in the discipline or in the topic group). But under the routes mentioned above and without having to create document types and content nodes. Otherwise I would have to create at least one new content node for each language and each category, hijack its URL and integrate my controller.

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 20:37
    pbl_dk
    0

    g..d.. I was pulling my hair out. I got it to work, its because of this line in master.cshtml

    @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Master>
    

    I think the view try to connect to that inherit and then returns that it doesnt exist.

    edit: Sorry, I mean the viewmodel doesnt exist. Anyway, when I deleted that line, the mvc worked as it should with Layout.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 21:05
    Dominic Resch
    0

    Hey, sry for the late reply. But I don't have this line ? So, can you tell me where it was initially coming from ?

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 21:10
    pbl_dk
    0

    yes, it was in my master.cshtml.. I cant pass ipublished content to the view at the moment, I can only pass on a hardcoded viewmodel.. But the mvc is ok.

    You have it here I think :

    @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
    
  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 21:18
    Dominic Resch
    0

    Interesting. I have now removed all the UmbracoPageViews (from index and master), but I still get the same exception...

    ModelBindingException: Cannot bind source type System.Collections.Generic.List`1[[Project.Umbraco.Models.SearchResultByTagModel, Project.Umbraco, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] to model type Umbraco.Cms.Core.Models.PublishedContent.IPublishedContent.

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 21:35
    pbl_dk
    0

    Let me clean up, then Ill upload to github. I have no idea how it works, but it returns Ipublished content to the view with a viewmodel now.

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 21:36
    Dominic Resch
    0

    Ok, thank you for the effort!

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 22:10
    pbl_dk
    0

    ok, see if you can fetch it.

    email: [email protected] pass: 1234567890

    the output is ~/clubs/

    https://github.com/pbldk/umbracothirteenipublishview

    I have just uploaded all of it..

  • Dominic Resch 45 posts 115 karma points
    Jan 19, 2024 @ 22:29
    Dominic Resch
    0

    Hey, yes, I downloaded it and it works. And I think the reason why it works for you is because your "ClubsPage" is a Document Type/Content Node. Meaning, you are in the context of a "Published Content", because the ClubsPage page itself is published content.

    The problem for me is when I now compare ClubsPage with my "Discipline page", for example: My "Page" is not a Document Type and not a Content Node. But ClubsPage is. I try to do all this without creating document types and content nodes by working with custom routes and views.

    Because otherwise, in the scenario above, 4 content nodes would already have to be created, namely for /de/disziplin, /en/discipline, /de/thema and /en/topic.

    And if several languages are added, I have to create even more. And then hijack the URL for each content node and set up a separate controller.

  • pbl_dk 150 posts 551 karma points
    Jan 19, 2024 @ 22:46
    pbl_dk
    0

    I am not sure you have to use ipublished content and make content, you should be able to just pass a viewmodel from anything.

    There is also some docs about custom routing here:

    https://docs.umbraco.com/umbraco-cms/reference/routing/custom-routes

    I am sure it can be done. I have to try it out.

  • Marc Goodson 2155 posts 14406 karma points MVP 9x c-trib
    Jan 19, 2024 @ 23:21
    Marc Goodson
    1

    Hi Dominic

    Umbraco is mainly expecting to be used as a Cms, so you define Document Types that define different types of pages for a website and which types of pages are allowed to be created underneath...

    When a page is published, then it's custom properties and core properties are represented by an IPublishedContent object and it is automatically routed by the position of the item in tree, so if you create a Homepage doctype and allow it to be created at root, and allow Standard page doc types underneath, then in the backoffice you can create instances of those page eg about us page and this would be automatically routed via /about-us to the selected template for the standard page doc type...

    ... I know you don't want to do this but it gives a bit of context...

    So on a basic level there is no need to add any custom routing rules or controllers...

    The reason I mention this is that for all requests through Umbraco there is the concept of there being a current IPublishedContent tied to the request... And by default this would be the published page in umbraco.

    But if you want to mix other external content to a page or create your own view models then you can create a controller that must inherit from Render Controller and follow the naming convention DocTypeAliasController

    And then without any Routing rules, any request to a page url based on that document type will be hijacked by the custom controller, you'll have reference to the IPublishedContent for the requested page, but up to you to build a view model and send it to a view...

    .. The key thing is you don't need a controller for each Url, just for the underlying Document Type, it will hijack all urs to pages of that type.

    ... But the flexibility is also there to make your IPublishedContent object come outside of Umbraco...

    Which is where you can map your own custom vitual routes, but you have to implement Find Content and either pick a dummy content item published in Umbraco to associate with the request or construct your own IPublishedContent item, which is where PublishedContentWrapped comes in as a starting point.

    This is good if you have fixed url patterns that won't change... But if you want something more flexible then you can create an implementation of IContentFinder, and add it to Umbraco incoming request pipeline... Here you can write your own custom rules for mapping a request to umbraco...

    ... It's hard to explain...

    But if you had an external library of products that you wanted to show in umbraco you could map a virtual route to /products/{id}

    But you'd be hardcoding that route...

    But if you used an IContentFinder you could map a request from /category1/product-id And the category could be edited in Umbraco...

    Not sure I've sold the benefits there

    ... Additionally when you create a site in Umbraco you can create different languages, and allow your Document Types to vary by culture, then by default umbraco routes the content on a unique url for each language culture...

    But you can completely ignore the cms and map mvc routes through, and associate dummy Ipublishedcontent, but it might be worth exploring having a Document Type called something like Section and allow it to vary by culture and create two pages called Discipline and Topic, and see what Umbraco gives you around this setup in terms of automatic routing and hijacking, in future if you have new language you just add it to umbraco, and it all automatically routes, another type of Theme, just add a new Section Content item..

    Good luck!

    Marc

  • Dominic Resch 45 posts 115 karma points
    Jan 22, 2024 @ 13:40
    Dominic Resch
    0

    Hello Marc!

    First of all, I would like to apologise for the late reply and thank you for your detailed answer!

    I understand that Umbraco works with document types and that you should always have some in the best case. I also know that these can vary in the culture. But it's true, I haven't given the impression in my previous answers. Nevertheless, I had hoped to be able to do this without them (without much effort).

    But anyway, I have now created document types (for discipline and topic) and content nodes based on these. It works so far.

    I now have a menu with all disciplines and topics. If I click on a discipline, I am shown topics. If I click on a topic, disciplines are shown.

    So far, so good, everything is working. I still have to change the document types to the same template and the like, but that should work.

    But now I still need another URL, namely:

    /{culture}/topic/{topic}/{discipline?}
    

    So I have created a TopicsController (derived from RenderController), with an Index and a Discipline Action. I use the HttpGet attribute to tell the Discipline action to listen to the above URL.

    My problem is that this action is never called and I don't understand why.

    Is the URL defined here simply ignored by Umbraco? Because when I call "/en-us/topic/tutorials", the action is called. However, if I then call "/en-us/topic/tutorials/architecture", the action is not called.

    Also, if I do this with the HttpGet attribute, then I have to hardcode the URLs with the respective languages again, which is not the point. Of course, I could now go and declare all disciplines with a new document type within my topics again. But I have ~10 disciplines and ~8 topics. If I have to define all disciplines for each topic, we're already talking about 80 new content nodes here alone. +10 for the menu.

    What do I actually want to achieve with all this?

    Simply put, a menu structure based on 2 factors: discipline and topic. If both discipline and topic are specified, I want to display all the articles that are in them. If only one of them is specified, the list of the missing one comes up for selection.

    Do you have any suggestion how I could achieve this? Any approach, an idea?

    Is this the point where I would need the IContentFinder? It would make no sense to me. Ultimately, I just want to "extend" the existing URL by simply attaching another URL segment.

    My TopicsController:

    public class TopicsController(
        ILogger<RenderController> logger,
        ICompositeViewEngine compositeViewEngine,
        IUmbracoContextAccessor umbracoContextAccessor) : RenderController(logger, compositeViewEngine,
        umbracoContextAccessor)
    {
        public override IActionResult Index()
        {
            return base.Index();
        }
    
        [HttpGet("/{culture}/thema/{topic}/{discipline?}")]
        public IActionResult Topic(string culture, string topic, string? discipline)
        {
            return CurrentTemplate(CurrentPage);
        }
    }
    
  • Marc Goodson 2155 posts 14406 karma points MVP 9x c-trib
    Jan 23, 2024 @ 10:24
    Marc Goodson
    0

    Hi Dominic

    Yes, it wasn't so much that you need to create DocTypes, but in your evaluation, if you did, you would get a better grok on how the routing works out of the box and therefore how you can alter it...

    There is so much I don't know about what you are ultimately trying to do :-P, even though you have explained! but in answer to your question:

    In terms of this observation:

    So I have created a TopicsController (derived from RenderController), with an Index and a Discipline Action. I use the HttpGet attribute to tell the Discipline action to listen to the above URL.

    My problem is that this action is never called and I don't understand why

    This is because when you inherit from RenderController to route hijack the convention is that your Action will match the alias of the template you are rendering this page. In Umbraco a Document Type can have multiple templates and you can create an Action to handle each one.

    Is this the point where I would need the IContentFinder? It would make no sense to me. Ultimately, I just want to "extend" the existing URL by simply attaching another URL segment.

    .Yes, this is that point!

    When a Url comes into Umbraco, there are a series of rules executed, in order, to try and match the incoming Url and match it to a published content item. These rules are created by implementing an interface called IContentFinder and then adding it inot the collection of ContentFinders in precedence order.

    Out of the box, Umbraco has an IContentFinder called ContentFinderByUrlAlias

    https://github.com/umbraco/Umbraco-CMS/blob/e04c4a89adcbe73d35290a993713a5faf4dc55ff/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs#L10

    which is currently responsible for routing your requests for your published topics and disciplines.

    The match happens in TryFindContent implementation that receives the raw incoming request and then queries the Umbraco published cache to find a matching segment in the position of the url.... but you can create your own and the rules can be whatever you need.

    so if you create a ContentFinderByVirtualTopic, and add it at the front of the content finder collection queue, then you can check the incoming URL

    /en-us/topic/tutorials/architecture

    and see if the first bit /en-us/topic/ matches the published content item, if so you know it's a valid Url that your ContentFinder should handle, and if not you return false, to allow the next ContentFinder in the queue to have a go.

    If it's one of your Urls, you can then read the rest of the Url and retrifve your architecture tutorials from your external system (I presume that's what you are trying to do?) and then if you don't find any, return 'false' the request will eventually fall through the other content finders and 404, but if you have some, then you can use the PublishedContentWrapped class to create an IPublishedContent item and populate it with your external Article Data, the IPublishedContent item has an underlying DocumentType and a Template, so you can set that here... (in this scenario I'll normally have a dummy DocumentType called VirtualPage or something, and I'll usually have a content item of the type published to use as a base for my virtual content)

    Anyway if your ContentFinder returns true, that's it, the virtual content gets sent through to Umbraco and you can 'hijack' it by creating a RenderController that matches it's underlying VirtualDocumentType...

    it's a very flexible way of extending Umbraco routing without having to 'hardcode the routes in an MVC way' and shows you can sort of do anything....

    https://docs.umbraco.com/umbraco-cms/reference/routing/request-pipeline/icontentfinder

    but it is also totally possible to create an implementation of IVirtualPageController and 'map' an MVC route to it - but in doing so you have to specify 'what' is the IPublishedContent context if for the route.

    So now you have published content you should be able to follow this example:

    https://docs.umbraco.com/umbraco-cms/reference/routing/custom-routes#custom-route-with-ivirtualpagecontroller

    or using ForUmbracoPage

    https://docs.umbraco.com/umbraco-cms/reference/routing/custom-routes#custom-route-with-forumbracopage

    to map your custom Urls to a specific a Controller and implement FindContent to return one of your published pages as the IPublishedContent context for the request.

    anyway hope that give you some more to play around with!

    regards

    Marc

  • Dominic Resch 45 posts 115 karma points
    Jan 23, 2024 @ 10:40
    Dominic Resch
    0

    Hi Marc,

    Thanks again for the reply!

    In fact, I was just about to try that again with IVirtualPageController and Co, now that I have a DocType for it.

    That I can create an action for each template is also good to know, thank you! I'll have to play around with that to see what I can do with it.

    Regarding the article data: No, it doesn't actually come from an external system. I have installed the Umbraco.TheStarterKit, which contains a blog with posts. I just want to be able to assign several "topics" and "disciplines" to these posts. Then I would simply like to have a menu (in the frontend), divided into disciplines and topics, which shows the user the relevant posts if both a discipline and a topic have been selected. If only one discipline has been selected, a list of topics should appear so that one can be selected. And vice versa. And as soon as both have been selected, a list of posts should be displayed in the respective discipline and topic.

    Thanks again for the answer and I'll get back to you as soon as I'm finished (hopefully with good news)!

Please Sign in or register to post replies

Write your reply to:

Draft