limo
Feb 12, 2018
visibility 7377
star star star star star star
(2 votes)

Access and modify the Remaining Url value on properties of type LinkItem, Episerver.Url and on UrlFragments in XHtmlStrings

Episerver version: 10.6.0

Episerver.CMS.UI version: 10.7.1

Background:

Since there was no Remaining Url segment available for Episerver.Url, LinkItem or XHtmlString fragment (i.e. link added in TinyMCE) in Episerver CMS prior to Episerver.CMS.UI version 10.6.0, we had implemented our on solution which was as follows:

For Links added in XHtmlString (tinymce) and for LinkCollectionItems, both based on LinkModel, we used the built-in Title property for typing in anchors or additional querystring parameters, and a custom tinymce plugin and a custom displaytemplate that built the url and added the title to the end of the href. (This was possible because we did not use the Title property for its correct purpose in the first place due to other constraints in the site architecture.) 

For properties of type Episerver.Url we had an additional string property that we used for adding anchors or querystring parameters when writing out the link in the view.

Now when Episerver has built-in support for RemainingUrl we wanted to switch to use this instead and remove our custom implementation. We wanted to do this using a scheduled job, since there were over 500 instances in our site content where the value was used and needed to be moved.

The questions I had to Episerver Support was:

1) How can I set the Query attribute of a property of type Episerver.Url, so that is available for the editors in the Remaining Url field in the Edit Link dialog? 

2) How can I set the Remaining Url property of a LinkItem or XHtmlString Fragment, so that it is available for the editors in the Remaining Url field in the Edit Link dialog?

I got excellent answers that helped me figure out the solutions below.

Please note that in the code examples below I am not checking wether there are values in the Query or Href properties already before adding our parameter values to it, as we had another separate script that checked this and assured it was empty.

Solution

Episerver.Url:

The Remaining Url value is stored in the Url.Query property. The Query property is read-only so in order to update an existing Url object you need to create a new Url object, set the Url.Path property value of the new Url to path value + query value and then replace the existing Url object with the new one. The Query property of the new Url will be automatically set by Episerver upon saving. The next time you access the Url programmatically the Query property will be set to the value you gave it, and the editors will see the value in the Remaining Url field in Edit View.

var itemsUsingLinkAddition = List<MyBlock>; //Pre-populated list of content items of type MyBlock that are using our custom property HeadingLinkAddition to add parameters to the URL.

foreach (var i in itemsUsingLinkAddition)
{
    var clone = i.CreateWritableClone() as MyBlock;
    try
    {
        if (!string.IsNullOrEmpty(clone.GetPropertyValue("HeadingLinkAddition")))
        {
            clone.HeadingLink = new Url(clone.HeadingLink.Path + clone.GetPropertyValue("HeadingLinkAddition"));
            clone.Property["HeadingLinkAddition"].Value = null;
        }

         _contentRepository.Save((IContent)clone, EPiServer.DataAccess.SaveAction.Publish, AccessLevel.Read);
    }
    catch (Exception)
    {
        //Log exception
    }
}

UrlFragment in XHtmlString:

There is no built-in method for this. You need to traverse the fragments and check if the fragment is of type UrlFragment. The UrlFragment is read-only so in order to update it you need to replace it with a new UrlFragment rather than update the existing. (In our case the value I wanted to get at was in the Title-fragment of the Url so I had to extract that from the previous fragment).

This is an example of how I updated xhtmlstring properties in pages. The same approach applies to Blocks, the only difference then is the need to cast to IContent before saving:

// Update the pages and publish/save depending on original status.
// pagesUsingUrl is a List<IContent> that I have filtered out prior to this step
foreach (var p in pagesUsingUrl)
{
    var contentItem = p.CreateWritableClone();

    try
    {
        for (var i = 0; i < contentItem.Property.Count(); i++)
        {
            if (contentItem.Property[i].Value != null && contentItem.Property[i].Value is XhtmlString)
            {
                XhtmlString xhtmlString = (XhtmlString)contentItem.Property[i].Value;
                for (var b = 0; b < xhtmlString.Fragments.Count; b++)
                {
                    if (xhtmlString.Fragments[b].GetType() == typeof(UrlFragment))
                    {
                        if (xhtmlString.Fragments[b] != null)
                        {
                            var originalUrlFragment = xhtmlString.Fragments[b].InternalFormat;
                            var thetitle = GetUrlFragmentTitle(xhtmlString.Fragments[b - 1].InternalFormat);

                            if (thetitle.StartsWith("?") || thetitle.StartsWith("#"))
                            {
                                // Update the url to have the title value as parameter
                                xhtmlString.Fragments[b] = new UrlFragment(originalUrlFragment + thetitle);

                                // Remove the original title value from previous fragment
                               var modifiedFragment = RemoveTitleFromFragment(xhtmlString.Fragments[b - 1].InternalFormat);

                               xhtmlString.Fragments[b - 1] = new StaticFragment(modifiedFragment);
                               xhtmlString.IsModified = true;
                           }
                       }
                   }
                }
            }
        }

        switch (contentItem.Status)
        {
            case VersionStatus.CheckedIn:
                _contentRepository.Save(contentItem, SaveAction.CheckIn, AccessLevel.NoAccess);
                break;
            case VersionStatus.DelayedPublish:
                _contentRepository.Save(contentItem, SaveAction.Schedule, AccessLevel.NoAccess);
                break;
            case VersionStatus.Published:
                _contentRepository.Save(contentItem, SaveAction.Publish, AccessLevel.NoAccess);
                break;
            case VersionStatus.Rejected:
                _contentRepository.Save(contentItem, SaveAction.Reject, AccessLevel.NoAccess);
                break;
            case VersionStatus.AwaitingApproval:
            case VersionStatus.CheckedOut:
            case VersionStatus.NotCreated:
            default:
                _contentRepository.Save(contentItem, SaveAction.Save, AccessLevel.NoAccess);
                break;
        }
    }
    catch (Exception)
    {
         // Log exception
    }
}

private string GetUrlFragmentTitle(string fragment)
{
    // This fragment should be something like '<span><a title=\"?blahblabh\" href=\"', as when you dissect the xhtmlstring it looks like 'dkasjdkasjdkl <a title="?blahblabh" href="~/link/43284yrhf3ff3y437hgfgt3fgt.aspx">'

    var titleValue = string.Empty;
    var strTitleStartMatch = "<a title=\"";
    var strTitleEndMatch = "\"";

    if (fragment != null)
    {
        if (fragment.Contains(strTitleStartMatch))
        {
            var startOfTitle = fragment.LastIndexOf(strTitleStartMatch, StringComparison.Ordinal) + strTitleStartMatch.Length;
            var endOfTitle = fragment.IndexOf(strTitleEndMatch, startOfTitle, StringComparison.Ordinal);

            titleValue = fragment.Substring(startOfTitle, endOfTitle - startOfTitle);
        }
    }
    return titleValue;
}

Image UrlFragment.png

LinkItem:

The Remaining Url value of a LinkItem is stored in the Href property. It is not read-only hence can be set just by updating linkItem.Href to href value + query value and saving the page/block. The editors will see the value of the query parameters in the Remaining Url field in Edit View, Episerver is managing that for you.

var clone = p.CreateWritableClone(); //where p is a content of a type that inherits PageData

for (var i = 0; i < clone.Property.Count(); i++)
{
    if (clone.Property[i] is EPiServer.SpecializedProperties.PropertyLinkCollection)
    {
        var propertyLinkCollection = clone.Property[i].Value as LinkItemCollection;
        foreach (LinkItem linkitem in propertyLinkCollection)
        {
            if (!string.IsNullOrEmpty(linkitem.Title) && (linkitem.Title.StartsWith("?") || linkitem.Title.StartsWith("#")))
            {
                // Set the Remaining Url attribute to the value of Title
                linkitem.Href = linkitem.Href + linkitem.Title;

                _contentRepository.Save(clone as IContent, EPiServer.DataAccess.SaveAction.Publish, EPiServer.Security.AccessLevel.NoAccess);
            }
        }
    }
}


Feb 12, 2018

Comments

error Please login to comment.
Latest blogs
Finding Thomas Part 5 - The Closed Loop

Five weeks. Five layers. One Thomas. If you've followed this series from the start — thank you. If you're just landing here, the short version:...

Ritu Madan | Jul 20, 2026

Extending the Optimizely Product Recommendations Feed to Include Custom Product Types

A practical way to extend the Optimizely Product Recommendations catalog feed so the export scheduled job also includes custom catalog types, like...

Wojciech Seweryn | Jul 20, 2026 |

Parallel Development in Optimizely CMS SaaS: A Smarter Way to Push Content Models

When Optimizely shipped the JavaScript SDK and CLI for CMS SaaS, they gave developers something pretty cool — a code-first workflow for content...

Vipin Banka | Jul 20, 2026

Fixing index_not_found_exception After Purging External Data in Optimizely Graph

The Scenario: Indexing External Data When working with Optimizely Content Graph, indexing external data is a straightforward process. Synchronize...

Akash Borkar | Jul 16, 2026

Finding Thomas Part 4 - The Intelligence Layer

I've been finding Thomas for a couple weeks now. Bear with me — we're almost at the full picture. Quick catch-up : Thomas is the returning visitor...

Ritu Madan | Jul 14, 2026

The Silent Success: When Your Optimizely SaaS CMS Config Push Succeeds with "0" Changes

  Picture this frustratingly common scenario in headless, code-first development with Optimizely SaaS CMS: You’ve defined a brilliant new element,...

Vipin Banka | Jul 13, 2026