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.
Gotcha’s using Custom Web Parts and the Minimal Download Strategy http://t.co/6H5uJN2UR1 #sharepoint
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;
}
}
Nice! Thanks for the addition.
Okay found nice solution to checking for a MDS call added null context and is MDS enabled checks : http://t.co/HzRPytbZ9Z #sp2013
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.
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.
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);
}
}
Great! Thanks for the addition.
Hey Steve, great post! Very helpful.
I have recently found that MDS is cancelled for any page which contains an app part. Have you seen this and do you have any ideas on what could be causing it?
I also posted this question on stackoverflow today, http://stackoverflow.com/questions/22693351/how-to-prevent-sharepoint-2013-app-parts-from-cancelling-minimal-download-strate.
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.
Steve,
I was getting the Mismatch Error in the ULS and found this site that explains not only what is going on, but how to fix it through the Central Admin settings.
http://www.codeovereasy.com/2014/02/mdsfailover-master-page-mismatch-solved/
Cool, thanks. Good to know!
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.