I was recently asked by a customer how they could pass some additional information to dynamic links (in this case, adding query string parameters to resolved component links). Easy I said, just extend the Tridion.ContentDelivery.Web.UI.ComponentLink web control, add a property for the parameters, and ensure this is added to the resolved link URL. I had in fact recommended this approach several times before, however I never actually implemented it myself. It was not quite as straightforward as I had expected, so this post shows you some code to get this done if you face a similar requirement.
My first step was to simply create a subclass of Tridion.ContentDelivery.Web.UI.ComponentLink add a Parameters property and type override to see what I could customize… I was hoping for a protected member like GetLinkUrl which would return the resolved url, allowing me to append my query string parameters, however there was just the Render method – it looked like I would have to rewrite the whole control.
Not wanting to re-invent the wheel I then decompiled the control to see how it is written in the first place. This showed that the Render method is rather simple:
protected override void Render(HtmlTextWriter writer) { Â Â if ((HttpContext.Current != null) && (HttpContext.Current.Application != null)) Â Â { Â Â Â Â using (ComponentLink link = new ComponentLink(new TcmUri(this.componentUri).PublicationId)) Â Â Â Â { Â Â Â Â Â Â string str = link.GetLinkAsString(this.pageUri, this.componentUri, this.templateUri, this.linkAttributes, "", this.textOnFail, this.addAnchor); Â Â Â Â Â Â base.WriteLinkTag(writer, str, this.textOnFail, this.linkText); Â Â Â Â } Â Â } }
Simple – but not very helpful to my case. As GetLinkAsString returns the whole link including the anchor tag, I would need to do some tiresome regex or similar to append the parameters to the URL. Secondly I discovered that the WriteLinkTag method is internal, which meant that I could not access it from a class outside of the Tridion.ContentDelivery assembly. Boo!
So I had to rewrite the linking logic myself, creating a Tridion.ContentDelivery.Web.Linking.ComponentLink object, initializing it with the parameters passed from the control, getting a link object back and, if resolved adding the parameters to the resolved url.
There was a little redemption here however, when I noticed that the Link class has a Parameters property. I turned out I just needed to set this property and the parameters would be added to the url for me.
Here is the full listing for the control
public class ComponentLink : Tridion.ContentDelivery.Web.UI.ComponentLink { // New Properties [Category("Appearance"), Bindable(true), DefaultValue("")] public string Parameters { get; set; } protected override void Render(System.Web.UI.HtmlTextWriter writer) { using (Tridion.ContentDelivery.Web.Linking.ComponentLink compLink = new Tridion.ContentDelivery.Web.Linking.ComponentLink(new Tridion.ContentDelivery.Web.Utilities.TcmUri(ComponentUri).PublicationId)) { Tridion.ContentDelivery.Web.Linking.Link link = compLink.GetLink(PageUri, ComponentUri, TemplateUri, LinkAttributes, "", TextOnFail, AddAnchor); link.Parameters = Parameters; if (!link.IsResolved) { if (TextOnFail) { writer.Write(LinkText); this.RenderChildren(writer); } } else { string linkString = link.ToString(); int index = linkString.IndexOf("</a>"); if (index > 0) { linkString = linkString.Substring(0, index); } writer.Write(linkString); writer.Write(LinkText); this.RenderChildren(writer); writer.Write("</a>"); } } } }
And there you go. This is just one scenario that you might want to extend linking, but there are many others. For example:
Creating Links to Dynamic Component Presentations (DCPs).
Suppose I am am a large retailer, and publish store location information as DCPs enabling site visitors to find stores using a map, or proximity search showing the appropriate store DCP when the search results are clicked. In this case there is no individual page for each Store component. If I wanted to link to a store from another page (perhaps a news article about an opening) the link would not resolve (as there is no page url to resolve to). I could enable store links to be created by having a special url (like /store?id=1234) where 1234 is the store component item ID, however these links would need to be hardcoded by editors, and would not be dynamic (if the store view url changed the links would break, if the store was removed the link would still show). I could however extend the ComponentLink control to have a DcpLink parameter, which contained something that I could use to determine that this should be handled differently (for example, the Store Schema ID, which I could map to a URL pattern /store?id={item-id} in my web app config). This would mean that links to stores could be handled in the normal way in the CMS and I would have the benefits of dynamic linking on the website.
Providing Fallback links (Language Selector)
I have quite often implemented language selectors for multi-language websites by using dynamic page links, simply by changing the publication id to the id of the other language publication(s). This means that if a page is published in other languages, it will appear in the language selector.
You may however, want to always show the language selector, even if the page is not available in another language, but in this case direct to a default page (like the home page) in the other language. Extending the PageLink control with a FallBackUrl parameter will enable you to add some logic to achieve this.
Let me know if you have run into any other scenarios or different approaches for extending linking.
I’ve used this trick in the past to trigger a health monitoring event, when component linking fails, to alert the web editors to the existence of a broken component link eg:
protected override void Render(HtmlTextWriter writer)
{
Tridion.ContentDelivery.Linking.ComponentLink link = new Tridion.ContentDelivery.Linking.ComponentLink();
String linkString = link.GetLinkAsString(base.PageURI, base.ComponentURI, base.TemplateURI, base.LinkAttributes, base.LinkText, base.TextOnFail, base.AddAnchor);
//broken component link
if (linkString == base.LinkText || linkString == String.Empty)
{
string _brokenLinkMessage = String.Format(“Component Linking Failed Between {0} and Component: {1}”, HttpContext.Current.Request.Url.AbsoluteUri, base.ComponentURI);
BrokenComponentLinkEvent _linkEvent = new BrokenComponentLinkEvent(_brokenLinkMessage, this, HttpContext.Current.Request.Url.AbsoluteUri, base.ComponentURI);
_linkEvent.Raise();
}
if (linkString.Contains(“/index.aspx”))
{
linkString = linkString.Replace(“/index.aspx”, “/”);
}
writer.Write(linkString);
}
(and to strip index.aspx page from the url for consistency in our google analytics reports)
Good use case Mark. It reminds me also of a requirement we had for a customer that wanted to strip the file extension (.aspx in this case) from all links, which we also did this way…
One more example is to handle NetBiscuits, an example here:. https://github.com/Dige/Tridion-NetBiscuits-Dynamic-Linking
The project also contains the backend part of the story (i.e. generating tags in your templates) because in this case the extended tag will behave like the default one unless it has an extra attribute or inner property.
Hi Will,
And what will happen to other links (PageLink, BinaryLink etc…) since we have to kind of de-register original Tridion.ContentDelivery.Web.UI and register the above custom Control then.
How can one register the above above custom control without losing existing functionality for other controls like: PageLink, BinaryLink …