Gotcha’s using Custom Web Parts and the Minimal Download Strategy

I’ve been playing around with some of my custom code in SharePoint 2013.  One of the issues I’ve been noticing is when I add any of my custom web parts to a page, the minimal download strategy (MDS) would failover to the normal asp.net webforms page.  You can tell the difference by looking at the url.  A url that looks like this:

/_layouts/15/start.aspx#/SitePages/Home.aspx

is using MDS.  Notice the actual page is start.aspx and then the page it is ajax loading (MDS) is /sitepages/home.aspx (the part after the hash(#)).  Whereas a normal asp.net webforms page url would look like this:

/SitePages/Home.aspx

They both look the same but when using MDS you get an added benefit of less being downloaded on each click and also smoother and snappier page changes.

Gotcha #1 – Decorate your assembly or class with MDSCompliant(true).  If your MDS isn’t working and you see this message in your ULS:

MDSLog: MDSFailover: A control was discovered which does not comply with MDS

then you will need to add the attribute (http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.webcontrols.mdscompliantattribute.aspx).  Here is an example of adding it to the class:

[MdsCompliant(true)]
public class MyWebPart : WebPart
{
	//...Code
}

And here is an example of adding it to an assembly (assemblyinfo.cs):

[assembly: MdsCompliant(true)]

Every control that is loaded on the page needs this attribute.  So if you are loading other controls in your web part, you’ll need to make sure they also have this attribute.

Gotcha #2 – Declarative user controls typically used for delegate controls need a codebehind and the attribute set as well.  I use a lot of delegate controls and then use features to swap out user controls in the delegate controls to add / remove functionality on the site.  Typically my user controls didn’t have a codebehind and would just add webparts, html or other controls in the markup.  The issue is if you want these controls to be able to be swapped out using MDS, then you will need to add a codebehind to the user control and decorate it with the MdsCompliant attribute.

So a normal user control like this:

<%@ Control Language="C#" AutoEventWireup="true"  %>
<%@ Register TagPrefix="MyWebParts" Namespace="MyWebParts"%>

<MyWebParts:WebPart runat="server" Title="WebPart"></MyWebParts:WebPart>

would need to be converted to this:

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="MyControl.ascx.cs" Inherits="MyControl" %>
<%@ Register TagPrefix="MyWebParts" Namespace="MyWebParts"%>

<MyWebParts:WebPart runat="server" Title="WebPart"></MyWebParts:WebPart>

and with the following codebehind:

[MdsCompliant(true)]
public partial class MyControl : UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
    }
}

I couldn’t figure out a way to decorate the usercontrol without using a code behind.  If anyone else knows how to do this, please comment or contact me with the info.  Thanks!

Gotcha #3 – Inline scripts are not allowed and will need to be added using the SPPageContentManager.  If you receive any of the following messages in your ULS logs you will need to look at your content.

MDSFailover: document.write
MDSFailover: document.writeln
MDSFailover: Unexpected tag in head
MDSFailover: script in markup

The first two are obvious; you can’t have any document.write or document.writeln’s in your html.  The third is a little less obvious.  According to a MS source, in the head these tags are not allowed:

  • Meta refresh tag
  • link tag for a stylesheet (text/css)
  • script tag
  • style tag
  • title tag
  • base tag

The fourth was a big kicker for me.  I had made the decision a few years ago to switch almost all of my webparts over to being XSLT rendered.  That means I had a lot of inline javascript and css in my xslt.  Luckily, I had previously also created a special webpart which could ajax load other webparts using an update panel and had solved the inline script issue before.  When I was writing my special ajax loading webpart before I found this page http://www.codeproject.com/Articles/21962/AJAX-UpdatePanel-With-Inline-Client-Scripts-Parser which showed how to extend the ootb update panel to automatically find inline scripts and register them to work correctly using ajax. I was able to slightly modify this code to work for my XSLT rendered webparts.

public bool IsAnAjaxDeltaRequest
{
	get
	{
		return false == String.IsNullOrEmpty(Context.Request.QueryString["AjaxDelta"]);
	}
}

protected override void Render(HtmlTextWriter output)
{
	base.Render(output);
	string html = GetHtml();
	if (IsAnAjaxDeltaRequest)
	{
		html = RegisterAndRemoveInlineClientScripts(this, this.GetType(), html);
	}
	output.Write(html);
}

public static readonly Regex REGEX_CLIENTSCRIPTS = new Regex(
"<script\\s((?<aname>[-\\w]+)=[\"'](?<avalue>.*?)[\"']\\s?)*\\s*>(?<script>.*?)</script>",
RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled |
RegexOptions.ExplicitCapture);

public static string RegisterAndRemoveInlineClientScripts(Control control, Type type, string htmlsource)
{
	if (htmlsource.IndexOf("<script", StringComparison.CurrentCultureIgnoreCase) > -1)
	{
		MatchCollection matches = REGEX_CLIENTSCRIPTS.Matches(htmlsource);
		if (matches.Count > 0)
		{
			for (int i = 0; i < matches.Count; i++)
			{
				string script = matches[i].Groups["script"].Value;
				string scriptID = script.GetHashCode().ToString();
				string scriptSrc = "";

				CaptureCollection aname = matches[i].Groups["aname"].Captures;
				CaptureCollection avalue = matches[i].Groups["avalue"].Captures;
				for (int u = 0; u < aname.Count; u++)
				{
					if (aname[u].Value.IndexOf("src",
						StringComparison.CurrentCultureIgnoreCase) == 0)
					{
						scriptSrc = avalue[u].Value;
						break;
					}
				}

				if (scriptSrc.Length > 0)
				{
					SPPageContentManager.RegisterClientScriptInclude(control,
						type, scriptID, scriptSrc);
				}
				else
				{
					SPPageContentManager.RegisterClientScriptBlock(control, type,
						scriptID, script);
				}

				htmlsource = htmlsource.Replace(matches[i].Value, "");
			}

		}
	}
	return htmlsource;
}

Since this code will automatically register any script references it finds, make sure that the paths to your scripts are correct, otherwise MDS will silently failover without any ULS messages.

Update 5/3/2013:

I have found another potential MDS error message in ULS:

MDSLog: Master page version mismatch occurred in MDS

or in the ajax response if using fiddler:

versionMismatch

I was able to resolve this by browsing to another site and then back to the original site with the issue.  Weird one, if anyone knows more about this error, please contact me or comment below.

15 Replies to “Gotcha’s using Custom Web Parts and the Minimal Download Strategy”

  1. Nice solution, I have also extended this with a null context and mds enabled checks:

    public bool IsAnAjaxDeltaRequest
    {
    get
    {
    if (!IsNullContext)
    {
    bool mdsEnabled = SPContext.Current.Web.EnableMinimalDownload;
    bool deltaRequest = String.IsNullOrEmpty(Context.Request.QueryString[“AjaxDelta”]);
    return mdsEnabled && !deltaRequest;
    }
    return false;
    }
    }
    public bool IsNullContext
    {
    get
    {
    return SPContext.Current == null;
    }
    }

  2. I have an idea that the master page version issue happens when the master is modified between refreshes, and the version changes. This is because with MDS you are trying to keep from re-rendering the whole page and do partial updates, but if the master changes, it needs to re-download the entire page.

    • Thanks, yea the message and what you say makes sense. The issue I noticed though is that I never changed the masterpage and this started happening. In addition, it kept giving me that error message each time it failed over which was on every click. You would think once it “refreshed” the masterpage it wouldn’t have to fail over anymore. It took me visiting another sp site and then coming back for it to “resolve” itself.

  3. Great article, thank you very much! But can you also show the content of the GetHtml()-function in your code snippet please?

    • The GetHtml() function is just a placeholder function for whatever html your webpart is going to render.

  4. This works for me now:

    protected override void Render(HtmlTextWriter output)
    {
    using (HtmlTextWriter htmlWriter = new HtmlTextWriter(new StringWriter()))
    {
    base.Render(htmlWriter);

    string html = htmlWriter.InnerWriter.ToString();
    if (IsAnAjaxDeltaRequest)
    {
    html = RegisterAndRemoveInlineClientScripts(this, this.GetType(), html);
    }

    output.Write(html);
    }
    }

  5. I haven’t but I also haven’t created any app parts. Try running a fiddler trace and you should see a partial message in the response for why it’s failing.

  6. I am having the same issue , Master pages are not being modifed manually. and links to the CDN are being shorted to releative urls . Recently i patched the servers and checking in the uls logs i found the error
    MDSLog: Master page version mismatch occurred in MDS: Context Page MP (currentMPToken) = [‘|SITES|EDRMS:|SITES|EDRMS|_CATALOGS|MASTERPAGE|XXX|CUSTOM_SEATTLE_V3.MASTER:3.15.0.0.0.15.0.4701.1000.7.FALSE.:en-US:en-NZ:RW’], MP supplied by page in URL (originalMPToken) = [‘|SITES|EDRMS:|SITES|EDRMS|_CATALOGS|MASTERPAGE|XXX|CUTSOM_SEATTLE_V3.MASTER:3.15.0.0.0.15.0.4569.1000.7.FALSE.:en-US:en-NZ:RW’], Current Request URL = [‘https://xxx/sites/EDRMS/Lists/yyy/DispForm.aspx?ID=1&Source=http://site/calendar.aspx&AjaxDelta=1’]

    Notice the SP version 15.0.47001.1000 this is the version we are on now, this happened to a select few from 250+ SC’s all had the ajax url set to relative and hence broke navigation /L&F. any ideas? I need to stop this from happening. Only way to see what sites are broken is to have ULS on in backgroud with a filter on it.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*