Showing posts with label Bug. Show all posts
Showing posts with label Bug. Show all posts

Friday, October 16, 2020

Problem in OTB groupstatus.aspx Sharepoint Online page and mobile browsers

In SPO there is one very useful OTB application layouts page groupstatus.aspx. Using this page we may redirect user to Notebook, Planner plan or Sharpeoint site associated with O365 group, e.g.

{siteUrl}/_layouts/15/groupstatus.aspx?target={target}

where {siteUrl} is url of the site associated with O365 group. And target may be "site", "notebook", "planner", etc. Full list may be found here: Generic URLs for all Office 365 Group connected workloads.
If we don't know url of the site associated with O365 group we may still use this page in context of the root tenant site and specify id of O365 group in additional query string parameter:

https://{tenant}.sharepoint.com/_layouts/15/groupstatus.aspx?target={target}&id={groupId}

 
In this case it will still redirect you to the requested target. And this is exactly how we use this page in TW - we open it in context of the root tenant site and provide target=site and id of the group in query string.
 
However at the same time Sharepoint has own redirection system for mobile devices: when you visit some Sharepoint site from mobile device it will redirect to special application layouts page optimized for mobile view \_layouts\Mobile\mblwpa.aspx (see Overview of Mobile Pages and the Redirection System). According to documentation this redirection for mobile devices is performed via http module (SPRequestModule). It means that in ASP.Net pipeline it will happen before request will come to groupstatus.aspx page. So when we redirect user to the root tenant site on mobile browser:

  https://{tenant}.sharepoint.com/_layouts/15/groupstatus.aspx?target={target}&id={groupId}

Sharepoint will just redirect user to mblwpa.aspx page in this root site

https://{tenant}.sharepoint.com/_layouts/15/mobile/mblwpa.aspx

and instead of seeing site associated with specified O365 group - user will see root tenant site. Will try to report this problem to MS using available communication channels. For now posted this problem on StackOverflow here.

Friday, June 12, 2020

Problem with threads count grow when use OfficeDevPnP AuthenticationManager

Recently we faced with the following problem in our Azure function app: after some time of functioning Azure functions got stuck and the only way to make them work again was to restart them manually from Azure portal UI. Research showed that problem was related with threads count: because of some reason threads count permanently grew to 6K thread after which Azure function app became unresponsive:

We reviewed our code, made some optimizations, change some async calls to sync equivalents, but it didn’t fix the problem. Then we continued troubleshooting and found the following: in the code we used OfficeDevPnP AuthenticationManager in order to get access token for communicating with Sharepoint Online like that:

using (var ctx = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(url, clientId, clientSecret))
{
    ...
}

Note that in this example new instance of AuthenticationManager is created each time when above code was executed. In our case it was inside queue-triggered Azure function which was called quite frequently. Elio Struyf (we worked over this problem together so credits for this finding go to him Smile) tried to make AuthenticationManager static – i.e. only one instance was created for the whole Azure function worker process:

public static class Helper
{
 private static OfficeDevPnP.Core.AuthenticationManager authMngr = new OfficeDevPnP.Core.AuthenticationManager();

 public static void QueueTriggeredFunc()
 {
  using (var ctx = authMngr.GetAppOnlyAuthenticatedContext(url, clientId, clientSecret))
  {
  ...
  }
 }
}

After that threads count got stabilized with avg 60 threads:

We reported this problem to OfficeDevPnP team and they confirmed that there is problem in AuthenticationManager which internally creates thread for monitoring access token’s lifetime, but this thread is not released when AuthenticationManager got recycled by garbage collector (also need to mention that AuthenticationManager itself was not disposable at the moment when this problem was found. We used OfficeDevPnP 3.20.2004.0 when found this problem). For now this workaround with making AuthenticationManager static was Ok and as far as I know currently OfficeDevPnP team works over correct fix for this problem. So hopefully it will be available soon globally.

Update 2020-06-15: from latest news I've got (thanks to Yannick Plenevaux :) ) OfficeDevPnP team addressed this issue in 202006.2 version and made AuthenticationManager disposable. So you may need to review your code in order to add using() around it.

Monday, March 26, 2018

Avoiding StackOverflowException when use assembly binding redirect in PowerShell

Recently I faced with interesting problem with assembly binding redirect in PowerShell: in my script I needed to use the following versions of the assemblies (these exact versions were used in other components and I couldn’t add another versions because of number of reasons):

OfficeDevPnP.Core 2.22.1801.0
Microsoft.Graph 1.7.0.0
Microsoft.Graph.Core 1.7.0.0

The problem is that OfficeDevPnP.Core 2.22.1801.0 references different versions of Microsoft.Graph and Microsoft.Grap.Core:

Microsoft.Graph 1.1.1.0
Microsoft.Graph.Core 1.2.1.0

So I needed to use assembly binding redirect. At first I tried approach described in the following post: Use specific version of Sharepoint client object model in PowerShell via assembly binding redirection:

$currentDir = Convert-Path(Get-Location)
$pnpCoreDir = resolve-path($currentDir + "\..\packages\SharePointPnPCoreOnline.2.22.1801.0\lib\net45\")
$graphDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.1.7.0\lib\net45\")
$graphCoreDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.Core.1.7.0\lib\net45\")

$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($pnpCoreDir, "OfficeDevPnP.Core.dll"))


<#$OnAssemblyResolve = [System.ResolveEventHandler] {
	param($sender, $e)
	
	if ($e.Name.StartsWith("Microsoft.Graph,"))
	{
		$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
		return $AssemblyGraph
	}
	if ($e.Name.StartsWith("Microsoft.Graph.Core,"))
	{
		$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
		return $AssemblyGraphCore
	}

	foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies())
	{
		if ($a.FullName -eq $e.Name)
		{
		  return $a
		}
	}
	Write-Host "Return null" -foregroundcolor red
	return $null
}
[System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)

However attempt to call method from loaded assemblies caused StackOverflowException and closing PowerShell session.

After that I tried C#-based assembly redirector (found it in the following forum thread: Powershell - Assembly binding redirect NOT found in application configuration file):

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

try
{
    $redirector = [Redirector]::new($null)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    Write-Warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

And surprisingly this approach worked. It looks like a bug in PowerShell script-based assembly binding redirect. Hope it will help someone.

Thursday, December 14, 2017

Problem with terminated site workflows which continue working in Sharepoint Online

Recently we faced with interesting issue in Sharepoint Online: there were several 2013 site workflows on different sub sites which worked this way:

  1. Iterate through publishing pages in Pages doclib of the current site and if current date is close to page’s Valid to date, workflow sends reminder email to responsible person
  2. Wait 1 day and then repeat iteration through pages

i.e. by itself workflow never ends. However it is possible to terminate workflow from UI: Site contents > Site workflows > click workflow > End workflow (see my previous post Restart site workflows in Sharepoint Online)

We published new version from Sharepoint Designer, ended previous workflow instances from UI and started new instances. And here we found interesting issue: terminated workflow instances continued to work, i.e. they continued to send emails of previous version to the users. In UI their status was displayed as Terminated. At the moment we contacted MS support but if you know about this issue please share it in comments.

Thursday, May 29, 2014

Add preview for pages with friendly urls in search results in Sharepoint 2013

Search preview is one of the nice features which was introduced in Sharepoint 2013. It gives possibility to see the target page without redirecting on that page. Preview may work for basic html pages or for documents (e.g. content of Word, Excel, Powerpoint documents can be shown in preview window). Some time ago we faced with one strange problem, which is platform bug from my point of view: for pages with friendly urls search preview doesn’t work. I.e. if for pages which have regular structural urls (http://example.com/pages/foo.aspx) preview panel looked like this:

image

then for pages with friendly urls defined in managed metadata term store (e.g. http://example.com/foo) it looked like this:

image

The key for solving this problem is in display template used for the page. Or to be more accurate, display template for hover panel used for the item. As you probably know for most items there are 2 display templates:

  1. for rendering item in search results;
  2. for hover panel for rendered item.

First “basic” display template has reference on second “supporting” display template in its code. By default Sharepoint uses standard Item_CommonItem_Body.html display template for items in search result (it may be overridden for different content types by search result types). Some time ago I wrote about how to modify this display template for fixing another Sharepoint problem, see Problem with cut titles in search results in Sharepoint 2013. In this article I will continue to work with the same display template, however you may use this approach with any other display template.

First of all let’s see from where problem with missing search preview for pages with friendly urls comes from. In order to do this we will need to check the code of Item_CommonItem_Body.html. First of all here is how javascript callback function is defined (this callback function is called when you click on the item in search result, see below):

   1:  
   2: var showHoverPanelCallback = ctx.currentItem_ShowHoverPanelCallback;
   3: if (Srch.U.n(showHoverPanelCallback)) {
   4:     var itemId = id + Srch.U.Ids.item;
   5:     var hoverId = id + Srch.U.Ids.hover;
   6:     var hoverUrl = "~sitecollection/_catalogs/masterpage/" +
   7:         "Display Templates/Search/Item_Default_HoverPanel.js";
   8:     showHoverPanelCallback = Srch.U.getShowHoverPanelCallback(itemId, hoverId,
   9:         hoverUrl);
  10: }

Later in the code it is used like this:

   1: <div id="_#= $htmlEncode(id + Srch.U.Ids.body) =#_" class="ms-srch-item-body" onclick="_#= showHoverPanelCallback =#_">
   2:     ...
   3: </div>

i.e. in onclick attrbiute of the item’s div. Second place where it is used is a link with item’s title:

   1: var titleHtml = String.format('<a clicktype="{0}" id="{1}" href="{2}"' +
   2:     'class="ms-srch-item-link" title="{3}" onfocus="{4}" {5}>{6}</a>',
   3:     $htmlEncode(clickType), $htmlEncode(id + Srch.U.Ids.titleLink),
   4:     $urlHtmlEncode(url), $htmlEncode(ctx.CurrentItem.Title), 
   5:     showHoverPanelCallback, appAttribs,
   6:     Srch.U.trimTitle(title, maxTitleLengthInChars, termsToUse));

Here it is used in onfocus attribute for the link which points to the target page.

Let’s check javascript code which gets callback function again. At first it retrieves it from ctx.currentItem_ShowHoverPanelCallback property and stores it to showHoverPanelCallback variable. Then it passes showHoverPanelCallback to Srch.U.n() function which is defined in Search.ClientControls.js like this:

   1: Srch.U.n = function Srch_U$n(obj) {
   2:     return SP.ScriptUtility.isNullOrUndefined(obj);
   3: }

and if result is true (i.e. if callback in ctx.currentItem_ShowHoverPanelCallback is null or undefined), it creates callback by itself. And it uses the following display template for the hover panel: ~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Default_HoverPanel.js.

Item_Default_HoverPanel.js is automatically generated javascript file from OTB display template Item_Default_HoverPanel.html. The problem is that this display template doesn’t support preview, i.e. it doesn’t have necessary code which loads the page into iframe and shows it. So if it is used, it technically can’t show preview. In order to show preview for pages another display template should be used: Item_WebPage_HoverPanel.html. It includes all necessary code and iframe for displaying the page like shown on the picture in the beginning of the article.

My first thought was just to replace Item_Default_HoverPanel.js on Item_WebPage_HoverPanel.js in the code above. However as it turned out code, which constructs callback function, is not called for most of the items (moreover it has a bug, because itemId variable defined as itemId = id + Srch.U.Ids.item will be incorrect. It will end with “_item_item” and will point to not-existent html element). It means that Srch.U.n() function returned false, i.e. callback was defined in ctx.currentItem_ShowHoverPanelCallback for most of the items. I added tracing with console.log() for ctx.currentItem_ShowHoverPanelCallback and found the following:

for pages with regular structural urls it contained something like this:

   1:  
   2: EnsureScriptParams('SearchUI.js', 'HP.Show',
   3: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_item', 
   4: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_hover',
   5: '~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002f' +
   6:     'Search\u002fItem_WebPage_HoverPanel.js', false);

while for pages with friendly urls it shows this:

   1: EnsureScriptParams('SearchUI.js', 'HP.Show',
   2: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_item',
   3: 'ctl00_SPWebPartManager1_g_2dd8e1bb_d0f9_486a_8daa_0e775d6c05cc_csr2_hover',
   4: '~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002f' +
   5:     'Search\u002fItem_Default_HoverPanel.js', false);

The key difference is in display templates used for hover panel: in first case Item_WebPage_HoverPanel.js is used which supports previews, while in second case Item_Default_HoverPanel.js is used where previews are not supported. After that another solution came to my mind: we need to construct callback function by ourselves for all items using Item_WebPage_HoverPanel.js display template. But how to do it? As callback is defined in ctx.currentItem_ShowHoverPanelCallback (which technically may be done on server side or in earlier steps in call stack on the client side), it may be hard to intercept the code where it is created. Much simpler to do it directly in display template.

Here is how it can be done. Instead of code shown above we need to use another code:

   1: var showHoverPanelCallback = ctx.currentItem_ShowHoverPanelCallback;
   2:  
   3: // change hover callback to WebPage for all items for showing preview
   4: try {
   5:     var idPrefix = id;
   6:     var suffix = "_item";
   7:     var idx = idPrefix.indexOf(suffix, idPrefix.length - suffix.length);
   8:     if (idx != -1) {
   9:         idPrefix = idPrefix.substring(0, idx);
  10:     }
  11:     var itemId = idPrefix + Srch.U.Ids.item;
  12:     var hoverId = idPrefix + Srch.U.Ids.hover;
  13:     var hoverUrl = "~sitecollection/_catalogs/masterpage/Display Templates/" +
  14:         "Search/Item_WebPage_HoverPanel.js";
  15:     showHoverPanelCallback = Srch.U.getShowHoverPanelCallback(itemId, hoverId,
  16:         hoverUrl);
  17:  
  18:     // we can access top-level div only asynchronously
  19:     AddPostRenderCallback(ctx, function(){
  20:         var itemContainer = document.getElementById(itemId);
  21:         itemContainer.setAttribute("onmouseover", showHoverPanelCallback)
  22:     });
  23: } catch (ex) {
  24:     console.log("Error occured in common item display template:" + ex);
  25: }

First of all we create correct ids of the divs for item itself and hover panel (lines 5-12). As I wrote above, code in OTB display template is incorrect: id variable which is used as a base already contains “_item” prefix, so we don’t need to add “_item” and “_hover” to it directly. After that for hoverUrl we use Item_WebPage_HoverPanel.js display template which supports previews. The last interesting part is how we attach created callback to the parent div, which is used as container for the item in search results. We can’t do it synchronously in the code, because this DOM element is not ready on the moment when the code is called. So we add it asynchronously by adding custom post render callback (lines 19-21).

After that you need to reupload Item_CommonItem_Body.html display template to /_catalogs/masterpage/Display templates/Search folder and publish it with major version. If you have several site collections you need to do it on all site collections separately. And the last thing: if you don’t want to change OTB display template, but instead want to create custom based on it and change this custom template, then in order to ensure that this display template is used on your search results page, you need to assign it to the ItemBodyTemplateId property of the ResultScriptWebPart on the target page for search requests:

   1: <property name="ItemBodyTemplateId" type="string">
   2:     ~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_CustomCommonItem_Body.js
   3: </property>

After that it should use your custom display template for the search items.

That is all which I wanted to write about fixing problem with search previews for pages with friendly urls in Sharepoint 2013. In future articles I will also show how to enable search preview for list items in search results, which is also not available by default.

Saturday, March 22, 2014

Problem with using not latest publishing version in cross-site publishing in Sharepoint 2013 site

In one of the previous project with cross-site publishing we faced with strange behavior when Sharepoint showed not latest publishing version in Content by search web part from search index. Here is the scenario: we used classic architecture with single authoring web application and multiple publishing sites:

image

On publishing sites managed metadata was used in order to show different authoring content on different publishing sites (e.g. Country term set and different publishing sites for different countries). In order to simplify content creation on authoring site custom ribbon action was implemented for Pages doclib which copied publishing pages in the same doclib on the authoring site. I.e. page created for all languages was copied, then translators translated this copy to another languages and changed Country managed metadata to appropriate language. Also they removed this language from original page’s metadata:

image

It worked quite well, i.e. English content was shown on English publishing site, Russian – on Russian. But some time after site worked in production we found one problem: on publishing site if user changed browser language, then Content by search web part showed original English version of the page, which was targeted to both languages. But as I wrote above after translators translated the page they removed their language from the original page (Page 1 on the picture above) and published it with major version.

We used continuous crawl which worked quite well in other scenarios and the problem still reproduced after several hours after it was found, i.e. continuous crawl didn’t fix it. We tried to make incremental and full crawls after that also without result. This behavior means the following:

1. Browse language is used for filtering content in Content by search web parts. This is not new finding, I wrote about how to avoid it here: Configure content by search web parts in Sharepoint 2013 to not use client language.

2. Sharepoint may show content from the search index which is not last published version. I.e. even if greater major versions exist for the page, Content by search web part may still show previous version. This looks like a bug, because only latest content should be used in search index.

In order to fix the problem we changed our content by search web parts to not use client language like described in the article above. After that Content by search web parts started to use specified language, not Accept-Language http header of client’s browser, but the original problem was still not resolved.

Hope that this information will be useful if you will face with the same problem when will work with cross-site publishing in Sharepoint.

Saturday, October 12, 2013

Problem with WebPartPageUserException in Sharepoint 2013 web parts

In Sharepoint 2013 web parts are created in different way comparing with previous versions. Now with .ascx and .ascx.cs files, which also were created before, there is new .ascx.g.cs file – automatically generated file which contains code for all elements added on .ascx file. I already wrote about one problem with new web parts here: Fix bug in Visual Studio 2012 with deleting generated ascx.g.cs files for web parts. In this post I will write about another problem, related with them. Your web part may work properly until you will add new element to ascx file. After that you may get the WebPartPageUserException:

The default namespace "http://schemas.microsoft.com/WebPart/v2" is a reserved namespace for base Web Part properties. Custom Web Part properties require a unique namespace (specified through an XmlElementAttribute on the property, or an XmlRootAttribute on the class).
   at Microsoft.SharePoint.WebPartPages.TypeCacheBuilder.CheckNamespace(String ns, PropertyInfo pi)
   at Microsoft.SharePoint.WebPartPages.TypeCacheBuilder.EnsureNamespace(PropertyInfo pi, XmlAttributes xmlAttrs)
   at Microsoft.SharePoint.WebPartPages.TypeCacheBuilder.CreateXmlSerializer(Storage storage, Boolean buildPersistedNames, Boolean shouldExcludeSpecialProperties, Hashtable& persistedNames, Hashtable& namespaces, Hashtable& dwpNamespaces)
   at Microsoft.SharePoint.WebPartPages.TypeCacheEntry.ThreadSafeCreateXmlSerializer(XmlSerializer& serializer, Storage storage, Boolean buildPersistedNames, Boolean shouldExcludeSpecialProperties)
   at Microsoft.SharePoint.WebPartPages.TypeCacheEntry.get_XmlSerializer()
   at Microsoft.SharePoint.WebPartPages.WebPart.ParseXml(XmlReader reader, Type type, String[] links, SPWeb spWeb)
   at Microsoft.SharePoint.WebPartPages.WebPart.AddParsedSubObject(Object obj)

I faced with this problem when added new <script> element with reference to javascript file on top of ascx. But it is not stable step to reproduce the problem. In the same project we had another web parts with added scripts already, which worked properly.

This problem comes from Microsoft.SharePoint.WebPartPages.WebPart, or more specifically from serializer used in this class. Because of some reason it “thinks” that we used reserved xml namespace "http://schemas.microsoft.com/WebPart/v2" for web part property in web part declaration in .webpart file, which is of course not true: in this case we don’t even have .webpart file with xml declaration. Instead we have ascx file and Sharepoint uses the same serializer, which it uses when deserialize web part from xml. The quick way to fix it is to change base class from Microsoft.SharePoint.WebPartPages.WebPart to System.Web.UI.WebControls.WebParts.WebPart. But if you want still use Microsoft.SharePoint.WebPartPages.WebPart because of some reason (in my practice I don’t remember the case when using of Microsoft.SharePoint.WebPartPages.WebPart gave advantage comparing with System.Web.UI.WebControls.WebParts.WebPart), you can use the following workaround.

First of all let’s see what code is added to .ascx.g.cs file when we add <script> tag into .ascx file:

   1: [global::System.ComponentModel.EditorBrowsableAttribute(global::System
   2: .ComponentModel.EditorBrowsableState.Never)]
   3: private void @__BuildControlTree(global::UPM.Internet.Raflatac.Web.UI.WebControls
   4: .WebParts.RibbonRecProduct.RibbonRecProduct @__ctrl) {
   5:     System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
   6:     @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl(
   7: "\r\n\r\n<div>\r\n    <script type=\"text/javascript\" src=\"/_layouts/15/test/js/test.js?r" +
   8:                 "el=1\"></script>\r\n</div>\r\n\r\n"));
   9:     global::System.Web.UI.HtmlControls.HtmlGenericControl @__ctrl1;
  10:     @__ctrl1 = this.@__BuildControldivSearch();
  11:     @__parser.AddParsedSubObject(@__ctrl1);
  12:     @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n\r\n"));
  13:     global::System.Web.UI.HtmlControls.HtmlGenericControl @__ctrl2;
  14:     @__ctrl2 = this.@__BuildControldivResults();
  15:     @__parser.AddParsedSubObject(@__ctrl2);
  16:     @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n\r\n"));
  17:     global::System.Web.UI.HtmlControls.HtmlGenericControl @__ctrl3;
  18:     @__ctrl3 = this.@__BuildControldivNoResults();
  19:     @__parser.AddParsedSubObject(@__ctrl3);
  20: }

As you can see script is added as LiteralControl on line 6. Let’s see now code of Microsoft.SharePoint.WebPartPages.WebPart.AddParsedSubObject() method, which is first method from stack trace from WebPart class:

   1: protected override void AddParsedSubObject(object obj)
   2: {
   3:     LiteralControl control = obj as LiteralControl;
   4:     if ((control != null) && !this._hasParsedLiteralControlDWP)
   5:     {
   6:         string text = control.Text;
   7:         if (text.Trim().Length > 0)
   8:         {
   9:             SPWeb web;
  10:             try
  11:             {
  12:                 web = Utility.CurrentWeb();
  13:             }
  14:             catch (InvalidOperationException)
  15:             {
  16:                 throw;
  17:             }
  18:             catch (Exception)
  19:             {
  20:                 throw new WebPartPageUserException(
  21: WebPartPageResource.GetString("InvalidWebPartChild"));
  22:             }
  23:             try
  24:             {
  25:                 Type type = base.GetType();
  26:                 EmbeddedXmlReader reader = new EmbeddedXmlReader(
  27: new StringReader(text), type, web);
  28:                 WebPart webPartFrom = ParseXml(reader, type, null, web);
  29:                 if (webPartFrom.UnknownXmlElements.Count > 0)
  30:                 {
  31:                     foreach (XmlElement element in webPartFrom.UnknownXmlElements)
  32:                     {
  33:                         if ((element.NamespaceURI == "http://schemas.microsoft.com/WebPart/v2")
  34: && ((string.Compare(element.Name, "Assembly",  true, CultureInfo.InvariantCulture) == 0)
  35: || (string.Compare(element.Name, "TypeName", true, CultureInfo.InvariantCulture) == 0)))
  36:                         {
  37:                             throw new WebPartPageUserException(
  38: WebPartPageResource.GetString("InvalidWebPartTag"));
  39:                         }
  40:                     }
  41:                 }
  42:                 this.InheritProperties(webPartFrom, reader.PropertyNames);
  43:                 this._hasParsedLiteralControlDWP = true;
  44:                 return;
  45:             }
  46:             catch (WebPartPageUserException)
  47:             {
  48:                 if ((SPContext.Current != null) && !SPContext.Current.IsDesignTime)
  49:                 {
  50:                     throw;
  51:                 }
  52:             }
  53:             catch (Exception)
  54:             {
  55:                 throw new WebPartPageUserException(
  56: WebPartPageResource.GetString("InvalidWebPartChild"));
  57:             }
  58:         }
  59:     }
  60:     base.AddParsedSubObject(obj);
  61: }

In the beginning of the method (see lines 3-4) it check whether the added control is LiteralControl and if yes, executes block of code, which looks like on the code of deserializing of web part from xml (WebPart webPartFrom = ParseXml(reader, type, null, web)) and then initialize properties of current web part from properties of deserialized web part (this.InheritProperties(webPartFrom, reader.PropertyNames)).

It sounds a little bit crazy, but above code looks like on the one more way of how we can initialize web part properties for web parts in Sharepoint. I.e. add xml declaration of web part (from .webpart file) directly to ascx file with properties initialization, and these values will be used for your web part. I tried to do it, but got the same WebPartPageUserException. Another (most likely) reason is that there is a bug in visual web parts in Sharepoint 2013. See below for more details.

At the moment based on the findings above we can apply the following workaround: enclose <script> element with <div runat=”server”></div> (runat=”server” is important, see below why). After this generated code will be changed:

   1: [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel
   2: .EditorBrowsableState.Never)]
   3: private global::System.Web.UI.HtmlControls.HtmlGenericControl @__BuildControl__control2() {
   4:     global::System.Web.UI.HtmlControls.HtmlGenericControl @__ctrl;
   5:     @__ctrl = new global::System.Web.UI.HtmlControls.HtmlGenericControl("div");
   6:     System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
   7:     @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl(
   8: "\r\n    <script type=\"text/javascript\" src= uts/15/test/js/test.js?rel=1\"></s" +
   9:                 "cript>\r\n"));
  10:     return @__ctrl;
  11: }

I.e. now it will use HtmlGenericControl instead of LiteralControl. If you would enclose it with <div></div> without runat=”server” it will still use LiteralControl and you will get the same error.

In order to complete the picture, let’s see what code is generated for another web part in the same project with <script> element, which works:

   1: private void @__Render__control1(System.Web.UI.HtmlTextWriter @__w,
   2: System.Web.UI.Control parameterContainer) {
   3:     @__w.Write(@"
   4:  
   5: <script type=""text/javascript"" src=""/_layouts/15/test/js/test.js""></script>
   6:  
   7: <div class=""..."">
   8: <p>");
   9: ...
  10: }

I.e. it uses completely different approach because of some reason: instead of adding text via LiteralControl or HtmlGenericControl, it writes it via HtmlTextWriter.

Let’s summarize open questions:

1. Why for one web part generated code adds text via HtmlTextWriter and it works, but for other via LiteralControl, which fails. Both web parts are in the same project, inherit the same Microsoft.SharePoint.WebPartPages.WebPart class and <script> element was added as first element in ascx file for both of them.

2. For what reason MS added ability to initialize web part properties from xml, added to ascx file. My first thought was that it was added for simplifying initialization of the properties for cloud environments (when you can’t provision .webpart file to the file system), but then I checked that this code exists in Sharepoint 2007 and 2010 (in Sharepoint 2007 it is a bit different, but uses the same idea). In order to say something we should get working example.

There may be some reasons for this behavior of course, but it also can be that this is just a bug in visual web parts code generation and it uses AddParsedSubObject() method where it should not use it (like in second example where it uses HtmlTextWriter). If you will have further information about this problem, please share it in comments.