Copied to clipboard

Flag this post as spam?

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


  • Tom Engan 430 posts 1169 karma points
    Feb 14, 2017 @ 12:41
    Tom Engan
    0

    Language selector for multilingual site (using "Relate to original" when copying content)

    I have a multilingual site with two languages. I see that when copying content with "Relate to original" checked, the table umbracoRelation are added with parentId and ChildId.

    Is there anyone who has a Razor solution when someone want to select another language if there's a related pages in another language, or at homepage if a page (in another language) not exists / not published?

  • Tom Engan 430 posts 1169 karma points
    Feb 14, 2017 @ 14:55
    Tom Engan
    0

    Another place, someone mentioned RelationService and Content Service meybe could be used instead, if so - how?

    RelationService: http://our.umbraco.org/documentation/reference/Management-v6/Services/RelationService

    ContentService: http://our.umbraco.org/documentation/reference/Management-v6/Services/ContentService

  • Tom Engan 430 posts 1169 karma points
    Feb 27, 2017 @ 15:29
    Tom Engan
    0

    Someone who can help me with a new verion of this?

    I found tis code here, but umbraco.cms.businesslogic.relation.RelationType is deprecated:

    http://sinfomania.com/tutorials/umbraco-7x/developer/multi-language-site/

    @inherits Umbraco.Web.Macros.PartialViewMacroPage
    @{
        // get current node
        var curNode = Umbraco.TypedContent(Model.Content.Id);
    
        // get language of current node
        string nodeLang = curNode.GetCulture().ToString().Substring(0, 2);
    
        // has current node relations created by copy as relation'?
        bool hasRel = umbraco.cms.businesslogic.relation.RelationType.GetByAlias("relateDocumentOnCopy").HasRelations(curNode.Id);
    }
    @if (hasRel) {
        <ul class="langSwitch">
            <li>@Umbraco.GetDictionaryValue("qv")</li>
            // get relations of current node
            var rels = umbraco.cms.businesslogic.relation.RelationType.GetByAlias("relateDocumentOnCopy").GetRelations(curNode.Id);
    
            @foreach (var rel in rels)
            {
                // english is the original language 
                // All other language pages are created via 'copy as relation'
                // if the language of the current node is "en", 
                // get the id of the child of the relation, 
                // otherwise get the id of the parent of the relation  
                var relId = (nodeLang == "en") ? rel.Child.Id : rel.Parent.Id;
                var relNode = Umbraco.Content(relId);
    
                // get the root node of the current relation node
                var topNode = Umbraco.TypedContent(relId).AncestorOrSelf(1);
    
                // get the language of the root node of the current relation node
                string tnLang = topNode.GetCulture().ToString().Substring(0,2);
    
                // exchange "en" with "gb" for the language indicator of the bootstrap flag
                string flagLang = (tnLang == "en") ? "gb" : tnLang;
                <li>
                    <button type="button" class="btn btn-navbar img-thumbnail flag flag-icon-background flag-icon-@flagLang"></button>
                    <a href="@relNode.Url">@relNode.Name</a>
                </li>
            }
        </ul>
    }
    
  • Tom Engan 430 posts 1169 karma points
    Feb 28, 2017 @ 12:02
    Tom Engan
    0

    Something like this instead since umbraco.cms.businesslogic.relation.RelationType is deprecated?

    @inherits Umbraco.Web.Macros.PartialViewMacroPage
      @{
        int currentPage = CurrentPage.Id;
        var rs = ApplicationContext.Current.Services.RelationService;
        var ids = rs.GetByParentOrChildId(currentPage);
    
        <li class="dropdown" data-dropdown="dropdown">
            <a href="@CurrentPage.AncestorOrSelf(1).Url" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">@Html.Raw(Umbraco.GetDictionaryValue("Språk"))<span class="caret"></span></a>
            <ul class="dropdown-menu" role="menu">
    
    
                @foreach (var item in ids.Where(r => r.RelationType.Alias == "relateDocumentOnCopy"))
                {
    
                  // Some codes missing here
    
                }
    
            </ul>
        </li>
    } 
    
  • Hugo 12 posts 146 karma points
    Feb 28, 2017 @ 14:53
    Hugo
    1

    I've solved this exact situation before. Your last example is definitely one way to go, using relationservice that way you can access all related nodes and create links for them. The GetByParentOrChild function returns a list of relations, so you'll need to select the parent/child nodes and get their ids. Then also remove duplicates. For each nodeid you have left you can let umbraco render the url for you.

    Be sure to check out this page: https://our.umbraco.org/documentation/reference/management/services/relationservice

    However, relationservice makes a call to the database. It will slow down your website if you use it in your views.

    To avoid the database call I hook into RelationService events for saving/deleting a relation, and update a content property on my documents. This way the ids for related documents get saved into my content cache, and I can query them from the view without calling the database.

  • Tom Engan 430 posts 1169 karma points
    Feb 28, 2017 @ 17:01
    Tom Engan
    0

    Thank you for your feedback.

    Do you know how to get ApplicationContext.Current.Services.RelationService to act as readonly?

    And how can we find the related nodes that are not published or does not exist?

    For info: The relations can be found in database table [dbo].[umbracoRelation], if someone want to know.

  • Hugo 12 posts 146 karma points
    Mar 01, 2017 @ 07:11
    Hugo
    0

    I do not know of any way of making RelationService readonly. If you want to prevent developers from writing to it you could wrap it in a custom object or service, and exposing only the methods you want. But this will not prevent anyone from calling the relationservice directly instead of your wrapper-service.

    As for nodes that do not exist, in that case I don't think a relation can exist, it will have to point to nothing, which I'm pretty sure is not possible through ordinary means. I would assume that deleting a node will also delete its relation. If that's not the case then the remaining relation will probably have a child or parent node value of null, so if you loop through it you can find them.

    Nodes that are not published are easy though. If you use relationservice or contentservice to get the nodes from your relations, it will expose a Published property which will tell you if it's published or not. Same goes for Trashed nodes (i.e. nodes in the recycle bin).

  • Tom Engan 430 posts 1169 karma points
    Mar 01, 2017 @ 11:56
    Tom Engan
    0

    Thanks.

    Readonly was only in terms of performance. You wrote: "To avoid the database call I hook into RelationService events for saving/deleting a relation, and update a content property on my documents. This way the ids for related documents get saved into my content cache, and I can query them from the view without calling the database.", so thats what I thinking about, but don't how the codes look like. Maybe this is not som important now..

    If relationship does not exist, the record does not exist in Table [dbo]. [UmbracoRelation]. This relationship will not exist if "Relate to original" is not checked when copy, if I understand this correctly.

    In Table [dbo]. [CmsDocument] I see fields named [published] and [newest], but the same id exists also in [dbo]. [UmbracoNode], so how and where should I:

    1: Separating out double tags?

    2: Excluding those in the trash?

    3 Include only the nodes who are published?

    The example uses ApplicationContext.Current.Services.RelationService, but how can we find only the published nodes? Is it IPublishedContent that must be used? If so - how?

    Haven't found any examples of this. Have some code examples on how to do this, step by step?

  • Hugo 12 posts 146 karma points
    Mar 01, 2017 @ 12:45
    Hugo
    1

    You seem very focused on the database structure, but you're not querying the database directly so in the end it doesn't really matter where it saves the data. You're not talking to the database, you're talking to the service and the types it returns.

    Let's start simple: If you have 2 languages, that means that each node has 1 relation hooking it up to its other language.

    If you call this in your razor you will get that one relation.

    var relation = rs.GetByParentOrChildId(Model.Content.Id).Single(r => r.RelationType.Alias == "relateDocumentOnCopy");
    

    It shouldn't return more relations unless you have linked more languages to your document.

    This relation contains the ids for the parentnode and the childnode:

     var childNode = Umbraco.TypedContent(relation.ChildId);
     var parentNode = Umbraco.TypedContent(relation.ParentId);
    

    Using Umbraco.TypedContent means it will get the node and all its properties from the content cache, so you will have access to all its content. If a node is trashed or unpublished, TypedContent will return null. Trashed and unpublished nodes are not saved in the content cache.

    You will have to compare those node ids to your currentpage nodeid to figure out which page you should link; you don't want to link itself, you want to link the translation. You also need to make sure they're not null. If either of those is null, there is no translation.

    if (childNode != null && parentNode != null)
    {
        if(childNode.Id == Model.Content.Id)
        {
            @:<a href="@parentNode.Url">Link to other language</a>
        }
        else
        {
            @:<a href="@childNode.Url">Link to other language</a>
        }
    }
    

    If your node contains a property containing what language it is in, you could print that in the a tag to show the user what language clicking the link will get you.

    If you have more than 1 other language, things get a litte bit more complicated.

  • Tom Engan 430 posts 1169 karma points
    Mar 01, 2017 @ 16:07
    Tom Engan
    0

    Thanks. I'm only focused on the database structure because I want to understand more of what is happening in the background, and I have more experience with SQL programming. But of cource, I should rather focus on the service, in the end.

    I got error on this line

    var ids = rs.GetByParentOrChildId(Model.Content.Id).Single(r => r.RelationType.Alias == "relateDocumentOnCopy");
    

    where I have for some reason dual registration in the [dbo].[cmsDocument] database enter image description here

    also marked as trashed in the [dbo].[umbracoNode] database enter image description here

    but no error when I replaced Single with FirstOrDefault:

    var ids = rs.GetByParentOrChildId(Model.Content.Id).FirstOrDefault(r => r.RelationType.Alias == "relateDocumentOnCopy");
    

    Most works now, but some few logical errors in some cases where not 1: 1 relationship (and chosen language should point to topnode in the same language if relations do not exist, or not published).

    This is my solution som far (will try to simplify..).

    @inherits Umbraco.Web.Macros.PartialViewMacroPage
    
    @{
        // get current node
        var curNode = Umbraco.TypedContent(Model.Content.Id);
    
        // get language of current node
        string nodeLang = curNode.GetCulture().ToString().Substring(0, 2);
    
        var rs = ApplicationContext.Current.Services.RelationService;
        var ids = rs.GetByParentOrChildId(Model.Content.Id).FirstOrDefault(r => r.RelationType.Alias == "relateDocumentOnCopy");
    
        var childNode = Umbraco.TypedContent(ids.ChildId);
        var parentNode = Umbraco.TypedContent(ids.ParentId);
    
        <li class="dropdown" data-dropdown="dropdown">
            <a href="@CurrentPage.AncestorOrSelf(1).Url" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">@Html.Raw(Umbraco.GetDictionaryValue("Språk"))<span class="caret"></span></a>
            <ul class="dropdown-menu" role="menu">
    
                @if (childNode != null && parentNode != null)
                {
                    if(childNode.Id == Model.Content.Id)
                    {
                        @:<li><a href="@parentNode.Url"><img class="flag flag-no" /> @Html.Raw(Umbraco.GetDictionaryValue("Norsk"))</a></li>
                    }
                    else
                    {
                        @:<li><a href="@childNode.Url"><img class="flag flag-gb" /> @Html.Raw(Umbraco.GetDictionaryValue("Engelsk"))</a></li>
                    }
                }
                else if (childNode == null || parentNode == null)
                {
                    if (nodeLang == "en")
                    {
                        @:<li><a href="@Umbraco.NiceUrl(1084)"><img class="flag flag-no" /> @Html.Raw(Umbraco.GetDictionaryValue("Norsk"))</a></li>
                    }
                    else if (nodeLang == "no")
                    {
                        @:<li><a href="@Umbraco.NiceUrl(7705)"><img class="flag flag-gb" /> @Html.Raw(Umbraco.GetDictionaryValue("Engelsk"))</a></li>
                    }
                }
            </ul>
        </li>
    }
    
  • Hugo 12 posts 146 karma points
    Mar 01, 2017 @ 20:53
    Hugo
    100

    Looks like you got it working mostly. You're right about trashed nodes, I forgot about that. TypedContent wil not return anything if you ask for a trashed node, but RelationService will.

    Also, FirstOrDefault is not a viable solution. You don't know for sure that the first one is the right one and not the trashed one. Here's some more (untested!) code that should help you out:

    var relations = rs.GetByParentOrChildId(Model.Content.Id).Where(r => r.RelationType.Alias == "relateDocumentOnCopy");
    var ids = relations.SelectMany(r => new[] { r.ChildId, r.ParentId }).Distinct();
    var nodes = ids.Select(n => Umbraco.TypedContent(n)).Where(n=> n != null);
    

    So first I get all relations that are connected to this node, because I don't know which one is good and which isn't. From that list of relations I create a list of all related node ids. Because each relation will also contain the current node id, in addition to the translated node, the current node id will be in that list several times. Trashed nodes and unpublished nodes will also be in that list.

    First I do a distinct to get rid of all the doubles. Then I use Select to create a new list that contains the return values for each nodeid when passed through TypedContent. So now I have a list of IPublishedContent mixed with null values. All trashed and unpublished id's will be replaced with null. If you filter those out you get a list of related nodes, which still also includes the current page.

    If it's a 1:1 translation, and any other relations for this node are trashed or unpublished, you will end up with 2 nodes: the current one and the translation one. You can hook that up into your current code. If there are more nodes that means you have more translations or something is related when it shouldn't be. If you need more translations you can do a for loop through this list (minus the currentpage) and generate links to the other pages.

  • Tom Engan 430 posts 1169 karma points
    Mar 02, 2017 @ 13:22
    Tom Engan
    0

    This is excellent. I'm really impressed. Before I write the whole solution to help for other Umbraco users, it's still something I have to figure out.

    I tested a node I've had trouble with, like this:

    var rs = ApplicationContext.Current.Services.RelationService;
    
    var relations = rs.GetByParentOrChildId(Model.Content.Id).Where(r => r.RelationType.Alias == "relateDocumentOnCopy");
    

    var relations (RelationService return trashed nodes):

    [0] id: 496 < ChildId 8164 (english, trashed node), ParentId 7486 (norwegian, published node)

    [1] id: 688 < ChildId 8266 (english, published node), ParentId 7486 (norwegian, published node)


    var ids = relations.SelectMany(r => new[] { r.ChildId, r.ParentId }).Distinct();
    

    var ids (Distinct filtered all the doubles, like the two 7486 abowe became one here):

    [0] id: 8164 < (english, trashed node)

    [1] id: 7486 < (norwegian, published node)

    [1] id: 8266 < (english, published node)


     var nodes = ids.Select(n => Umbraco.TypedContent(n)).Where(n => n != null);
    

    var nodes (TypedContent doesn't return trashed nodes):

    [0] Content id: 7486, Name: "Lyngdal" < ParentId (norwegian, published node)

    [1] Content id: 8266, Name: "Lyngdal" < ChildId (english, published node)


    This seems right. But the last part: How to write these two Ids, since

     var childNode = Umbraco.TypedContent(nodes.ChildId);
     var parentNode = Umbraco.TypedContent(nodes.ParentId);
    

    don't work (applies to: nodes.ChildId and nodes.ParentId that's not valid)?

  • Hugo 12 posts 146 karma points
    Mar 02, 2017 @ 16:14
    Hugo
    1

    You've already called TypedContent, so you already have the typed nodes with the url property.

    For each entry in your nodes enumeration you can call .Url to get the url. You will need to be able to recognize which node belongs to which language for your dictionary keys, easiest would be to just have the node culture for keys. Using a loop you can easily implement more translations, instead of having to write another if for each translation.

    So something like:

    var nodes = ids.Select(n => Umbraco.TypedContent(n)).Where(n=> n != null && n.Id != Model.Content.Id);
    if(nodes.Any())
    {
        foreach(var node in nodes)
        {
            var key = node.GetCulture().ToString();
            @:<li><a href="@node.Url"><img class="flag flag-no" /> @Html.Raw(Umbraco.GetDictionaryValue(key))</a></li>
        }
    }
    else 
    {
          //if there aren't any translations, your links to 1084 and 7705
    }
    
  • Tom Engan 430 posts 1169 karma points
    Mar 03, 2017 @ 12:42
    Tom Engan
    1

    You've been very helpful, and I'm very grateful for that. That's a very good idea you have there.

    Here's the solution that seems to work just the way I wanted, but have not tested this yet with more than two languages.

    Macro: LanguageSelector.cshtml

    @inherits Umbraco.Web.Macros.PartialViewMacroPage
    
    @{   
        // RelationService returns relations from published, unpublished and trashed nodes 
        var rs = ApplicationContext.Current.Services.RelationService;
    
        // Gets all relations that are connected to this node copied with "Relate to original"
        var relations = rs.GetByParentOrChildId(Model.Content.Id).Where(r => r.RelationType.Alias == "relateDocumentOnCopy");
    
        // Creates a list of all related node ids of trashed, unpublished and published nodes.
        // Each relation also contain the current node id that will be in that list several times.
        // Distinct filters all the doubles current nodes (but still contains trashed and unpublished nodes)
        var ids = relations.SelectMany(r => new[] { r.ChildId, r.ParentId }).Distinct();
    
        // TypedContent only return published nodes, not unpublished and trashed
        // Select creates a new list that contains the return values for each nodeid when
        // passed through TypedContent (a list of IPublishedContent mixed with null values).
        // All trashed and unpublished id's will be replaced with null, and only the published left.
        // != Model.Content.Id filtered away the current node, only one node pr language left?
        var nodes = ids.Select(n => Umbraco.TypedContent(n)).Where(n => n != null && n.Id != Model.Content.Id);
    
        // Get current node
        var curNode = Umbraco.TypedContent(Model.Content.Id);
    
        // Get language of current node
        string nodeLang = curNode.GetCulture().ToString().Substring(0, 2);
    
        // If nodeLang = "no" then flagcode = "no" else flagcode = "gb" 
        var flagcode = (nodeLang == "no") ? "no" : "gb";
    
        <li class="dropdown" data-dropdown="dropdown">
            <a href="@CurrentPage.AncestorOrSelf(1).Url"><img class="flag flag-@flagcode" /><span class="caret"></span></a>
            <ul class="dropdown-menu" role="menu">
            @{
              flagcode = (nodeLang == "no") ? "gb" : "no"; 
              if (nodes.Any())
              {
                foreach (var node in nodes)
                {
                  @:<li><a href="@node.Url"><img class="flag flag-@flagcode" /> @Html.Raw(Umbraco.GetDictionaryValue("Språk"))</a></li>
                }
              } else {
                int homeid = (nodeLang == "en") ? 1084 : 7705;
                @:<li><a href="@Umbraco.NiceUrl(homeid)"><img class="flag flag-@flagcode" /> @Html.Raw(Umbraco.GetDictionaryValue("Språk"))</a></li>
             }}
            </ul>
        </li>
    }
    
Please Sign in or register to post replies

Write your reply to:

Draft