Second Life of a Hungarian SharePoint Geek

April 30, 2018

Creating statistics about web part usage from the SharePoint content database

Filed under: PowerShell, Reflection, SP 2013, SQL, Web part — Tags: , , , — Peter Holpar @ 21:08

Recently I had to create some statistics about SharePoint web site customizations, like on which pages are there Script Editor Web Parts, or Content Editor Web Parts, etc. I knew I could and probably should have done it by iterating through all web sites, all pages and then looking up the web parts on each page using SPLimitedWebPartManager class, but I was aware, the same information should be available via the content database as well, making it possible to query the info much easier and faster, although unsupported. In this post I describe, how you can do it, but use the solution at your own risk.

The web part information is stored in the AllWebParts table, the information about the pages in the AllDocs table. I joined these tables together for the first report about the Script Editor Web Parts.

SELECT AD.DirName + ‘/’ + AD.LeafName as PageUrl, AWP.tp_ZoneID as ZoneId, AWP.tp_PartOrder as WebPartOrder, AWP.tp_Class AS WebPartClass
FROM
AllWebParts AWP (nolock)
INNER JOIN AllDocs (nolock) AD ON AWP.tp_SiteId = AD.SiteId AND AWP.tp_PageUrlID = AD.Id
WHERE tp_Class LIKE ‘%ScriptEditorWebPart’

Next, I was to create a report about the Content Editor Web Parts, using a filter like:

WHERE tp_Class = ‘%ContentEditorWebPart’

However, no result found, although I was pretty sure, there are a lot of them in our web site. How is it possible?

To test it further, I’ve included a Script Editor Web Part and a Content Editor Web Part on the AllItems.aspx page of the Tasks list in one of our sub-site, and created a new query with the filter below:

WHERE DirName LIKE ‘%site/subsite/Lists/Tasks%’
AND LeafName LIKE ‘%AllItem%’

This was the result:

image

As you see, the Script Editor Web Part is there, and you see two further web parts (they should be the Content Editor Web Part and the XsltListViewWeb part, that was originally on the page and is responsible to display the task items in the list), however both of them with a NULL value in the WebPartClass column. What should it mean?

I have studied the structure of the AllWebParts table and the relations of its fields further, and found that there are two fields (tp_Class and tp_Assembly) that are always populated for the records, where the WebPartClass is not NULL, and there is a field called tp_WebPartTypeId – populated for each entries, even for those, where the WebPartClass , tp_Class and tp_Assembly fields are empty – that we could eventually use to find the matching web parts. But how? I made a search for ‘WebPartTypeId’ using .NET Reflector, and found the internal class Microsoft.SharePoint.WebPartPages.WebPartTypeInfo, having a private static method called GetWellKnownTypeIdDictionary that returns a Dictionary<Guid, Type> mapping Guids (WebPartTypeIds) to the actual web part type. Remark: The Guids in the WebPartTypeId are actually created from the MD5 hash of the bytes of the joined full assembly name and web part class name, see the internal static  GetTypeIdUnsafe(MD5HashProvider md5Provider, string typeFullName, string assemblyName) method of the internal sealed class Microsoft.SharePoint.ApplicationRuntime.SafeControls.

image

To support those so called well-known types in my former SQL-query, I wrote a short PowerShell script that invokes the private static GetWellKnownTypeIdDictionary method of the internal WebPartTypeInfo class, and emits the resulting Dictionary to a text file I can use to extend my query:

  1. $webPartTypeInfoType = [System.Type]::GetType('Microsoft.SharePoint.WebPartPages.WebPartTypeInfo, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c')
  2. $mi_GetWellKnownTypeIdDictionary = $webPartTypeInfoType.GetMethod('GetWellKnownTypeIdDictionary', [Reflection.BindingFlags]'NonPublic, Static')
  3. $wellKnownTypeIdDictionary = $mi_GetWellKnownTypeIdDictionary.Invoke($null, $null)
  4.  
  5. $wpTypes = $wellKnownTypeIdDictionary.Keys | % { "INSERT INTO @WPTypes VALUES ('$_', '$($wellKnownTypeIdDictionary[$_].Assembly.FullName)', '$($wellKnownTypeIdDictionary[$_].FullName)')" }
  6. Set-Content -Path 'C:\Data\WPTypes.txt' -Value $wpTypes

And that is already the extended version of the SQL query:

  1. DECLARE @WPTypes TABLE
  2.    (
  3.      Id uniqueidentifier NOT NULL,
  4.      AssemblyName varchar(500),
  5.      ClassName varchar(100)
  6.    )
  7.  
  8. INSERT INTO @WPTypes VALUES ('8e20cf70-0fd5-1e08-9972-38f63a6bd59a', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ImageWebPart')
  9. INSERT INTO @WPTypes VALUES ('ba009853-eac3-16c8-9094-a8834485ad33', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.DataFormWebPart')
  10. INSERT INTO @WPTypes VALUES ('83216ab2-cd0e-e9fc-fc5e-6a8f3b21c37b', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.DataViewWebPart')
  11. INSERT INTO @WPTypes VALUES ('42fddde2-e0cf-c8ab-48b7-db1fcac0a917', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ListFormWebPart')
  12. INSERT INTO @WPTypes VALUES ('05d0fd94-372a-5ee7-b480-ccb8f9cd2c23', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ListViewWebPart')
  13. INSERT INTO @WPTypes VALUES ('aef28218-44f8-0538-9805-4842c0e62811', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.XsltListFormWebPart')
  14. INSERT INTO @WPTypes VALUES ('a6524906-3fd2-ee4e-23ee-252d3c6e0dc9', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.XsltListViewWebPart')
  15. INSERT INTO @WPTypes VALUES ('0c6143a7-d68b-bade-e0ef-2c4d01182b0c', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.BlogAdminWebPart')
  16. INSERT INTO @WPTypes VALUES ('afef48e1-8f94-eb71-03a6-ffceb685306a', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.BlogMonthQuickLaunch')
  17. INSERT INTO @WPTypes VALUES ('4c06cea2-364f-47e3-e1d7-08d53f441157', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ContentEditorWebPart')
  18. INSERT INTO @WPTypes VALUES ('e6047383-438e-ed87-1a93-f1ff71729044', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.TitleBarWebPart')
  19. INSERT INTO @WPTypes VALUES ('707c1e73-0b3d-898b-c755-01621802ab8c', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.SilverlightWebPart')
  20. INSERT INTO @WPTypes VALUES ('28c23aec-2537-68b3-43b6-845b13cea19f', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  21. INSERT INTO @WPTypes VALUES ('8d6034c4-a416-e535-281a-6b714894e1aa', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  22. INSERT INTO @WPTypes VALUES ('8e814083-396a-e7d1-148b-316e3a7283f7', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  23. INSERT INTO @WPTypes VALUES ('e6377261-6920-bbfe-501f-fda7a61db10f', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  24. INSERT INTO @WPTypes VALUES ('8efd140d-eae9-5feb-06e3-f771842d2e43', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  25. INSERT INTO @WPTypes VALUES ('b3294a07-46bf-e661-d036-10670590bbd3', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.SPUserCodeWebPart')
  26.  
  27. SELECT AD.DirName + '/' + AD.LeafName as PageUrl, AWP.tp_ZoneID as ZoneId, AWP.tp_PartOrder as WebPartOrder, ISNULL(AWP.tp_Class, WPT.ClassName) AS WebPartClass
  28. FROM AllWebParts AWP (nolock)
  29. INNER JOIN AllDocs AD (nolock) ON AWP.tp_SiteId = AD.SiteId AND AWP.tp_PageUrlID = AD.Id
  30. LEFT JOIN @WPTypes WPT ON AWP.tp_WebPartTypeId = WPT.Id
  31. WHERE ISNULL(AWP.tp_Class, WPT.ClassName) LIKE '%ContentEditorWebPart'

Of course, you can change the conditions of the query as you like, for example, you can restrict it to two web part type, like:

WHERE ISNULL(AWP.tp_Class, WPT.ClassName) IN (‘Microsoft.SharePoint.WebPartPages.ScriptEditorWebPart’, ‘Microsoft.SharePoint.WebPartPages.ContentEditorWebPart’)

There are a few more columns in the AllWebParts table, that you eventually would include either in the SELECT statement or in its WHERE clause, these are:

  • tp_IsIncluded: The web part is displayed on the page, if the value is 1 (default). If you close (not delete!) a web part, the value is 0. Deleted web parts are removed from the table.
  • tp_Deleted: Assume you have a list with some pages that includes web parts, like view pages including XsltListViewWebPart instances. The web part entries in the AllWebParts table have a value of 0 at this stage. If you delete the list, these values change to 1. The web part entries will be kept in the table even after deleting the list from the first (user) level Recycle Bin, and removed only after the list is deleted from the second (site collection) level Recycle Bin.
  • tp_ListId: This is a field that is populated for list-related built-in web parts, like XsltListViewWebPart. You can look up the related list and web instances by joining the Lists and Webs views in your query respectively, as shown below (this time I omit the declaration of the @WPTypes variable and its population with value for the sake of brevity, but of course, you need it this time either):

SELECT L.tp_Title as ListTitle, W.FullUrl AS WebUrl, AD.DirName + ‘/’ + AD.LeafName as PageUrl, AWP.tp_ZoneID as ZoneId, AWP.tp_PartOrder as WebPartOrder, ISNULL(AWP.tp_Class, WPT.ClassName) AS WebPartClass, tp_ListId
FROM AllWebParts AWP (nolock)
INNER JOIN AllDocs AD (nolock) ON AWP.tp_SiteId = AD.SiteId AND AWP.tp_PageUrlID = AD.Id
LEFT JOIN @WPTypes WPT ON AWP.tp_WebPartTypeId = WPT.Id
LEFT JOIN Lists L (nolock) ON AWP.tp_SiteId = L.tp_SiteId AND AWP.tp_ListId = L.tp_ID
LEFT JOIN Webs W (nolock) ON AWP.tp_SiteId = W.SiteId AND L.tp_WebId = W.Id
WHERE ISNULL(AWP.tp_Class, WPT.ClassName) LIKE ‘%ListViewWebPart’

By including the list title or the web URL in the WHERE clause (or the ID of the list or the web if you wish) you can further limit the items returned by the query.

If there are records returned with NULL in the ListTitle and WebUrl columns it means typically that the list was deleted, but yet available in the Recycle Bin. See my comments regarding the tp_Deleted field above. Note, that despite the name of the FullUrl column in the Web view, it is actually a server relative URL.

I hope this overview has helped you to better understand what and how is stored in these tables of the SharePoint content database, as well, how the “magical” IDs of the well-known web part types do fit into the whole picture.

May 8, 2011

Hunting for the lost Advanced search link

Filed under: Dynamic method, Reflection, Search, SP 2010, Web part — Tags: , , , , — Peter Holpar @ 01:37

The other day I had to configure SharePoint search web parts. The plans were to create a Search Center, and set each web parts to include the Advanced search link. After configuring the People Search Box web part I realized that nothing seems to be changed on the UI.

image

I’ve double-checked the settings and found that the Display advanced search link checkbox was checked and the URL of the advanced search page was specified in the Advanced Search page URL text box, so the advanced link should be there.

image

I found a question on the web about the same issue (based on its date, probably for MOSS 2007) but no answer. So I started Reflector to see what’s happening in the web part.

Here are the results:

PeopleSearchBoxEx class (Microsoft.SharePoint.Portal.WebControls namespace, Microsoft.SharePoint.Portal assembly) is responsible for rendering the People Search Box web part. This class is inherited from the SearchBoxEx class (Microsoft.SharePoint.Portal.WebControls namespace, Microsoft.Office.Server.Search assembly) that is behind the standard Search Box web part, where the Advanced link was displayed with the very same settings.

image

There is an internal virtual CreateAdvanceSearchLink method in the SearchBoxEx class that is overridden in the PeopleSearchBoxEx class. This method is called from the CreateChildControls method of the corresponding class. I suspected that the issue is somewhere there, and yes, although the SearchBoxEx version of the CreateAdvanceSearchLink method handles the advanced search link settings, the PeopleSearchBoxEx version does not, nor does it call its base class version.

So next question, how to fix this problem? In the following I show you a solution for that, although I have to admit, it is neither trivial nor probably supported, as it uses techniques that may fail after a SharePoint service pack. I suggest you to use it at your own risk, if you decide to use it at all. My real goal of publishing this workaround is to show you some “dirty” programming methods that may help you to give solutions in such situations.

If you are a regular reader of my posts, you may already know that I like using reflection to call hidden methods to enable features that is not enabled through the public SharePoint API. In this case this was my first intention either.

The CreateAdvanceSearchLink method has three parameters. The first one is a TableRow, it is the row of the search box, and the method adds a new cell to the row if there is a link to display. The second parameter is a String, it is the search keyword that is typically got from the k query string parameter and appended to the advanced search link URL. Although this parameter exists for the PeopleSearchBoxEx class version as well, it is not used in that implementation. The last parameter is an integer value that is passed by reference. It contains the actual cell count of the row, and is incremented if a new cell is added to the row in the method. It might be not the most elegant way of handling that but it works this way.

So I created a custom web part derived from the PeopleSearchBoxEx class and was to call the original CreateAdvanceSearchLink version (in the base-base class SearchBoxEx) using reflection that would create the lost link for me if the web part settings require that.

This code gets the  MethodInfo of the the  internal CreateAdvanceSearchLink method of the SearchBoxEx type, casts the current web part instance to the SearchBoxEx class and invoke the MethodInfo on that instance. It typically looks like that:

  1. protected void CreateAdvanceSearchLink(TableRow row, string keyword, ref int colsRest)
  2. {
  3.     Type searchBoxExType = typeof(SearchBoxEx);
  4.     Type[] parameterTypes = { typeof(TableRow), typeof(String), typeof(int).MakeByRefType() };
  5.     MethodInfo mi_CreateAdvanceSearchLink = searchBoxExType.GetMethod("CreateAdvanceSearchLink",
  6.             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, parameterTypes, null);
  7.     if (mi_CreateAdvanceSearchLink != null)
  8.     {
  9.         object[] args = { row, keyword, colsRest };
  10.         SearchBoxEx searchBoxEx = (SearchBoxEx)this;
  11.         mi_CreateAdvanceSearchLink.Invoke(searchBoxEx, args);
  12.         colsRest = (int)args[2];
  13.     }
  14. }

After the first test I found, that in this case not the SearchBoxEx version of the CreateAdvanceSearchLink method is called, but the PeopleSearchBoxEx version. That is because the original version is marked as virtual, so even reflection calls the overridden version.

What can we do in this case to force the .NET framework to call the original version? One can find the answer for this question in this forum thread. Although the answer from Doggett is not the accepted answer for the question at the time of writing this post, I found it useful to find the right way.

So let’s try the same using a dynamic method!

After declaring the delegate that corresponds to the signature of the CreateAdvanceSearchLink method (including the type itself), I created a member variable to store the reference to the delegate that I create in the class constructor. That is reasonable, as it does not change between calls (if we were to call it multiple times) so it helps better performance.

In this case we get the MethodInfo similarly to our first try, then use the ILGenerator class to generate the DynamicMethod. Finding the correct IL code requires some knowledge of the CLR and usually done through the trial and error approach (at least, for me).

Finally, we create the delegate instance using our DynamicMethod.

  1. delegate void CreateAdvanceSearchLinkDelegate(PeopleSearchBoxEx peopleSearchBoxEx, TableRow row, String keyword, ref int colsRest);
  2.  
  3. CreateAdvanceSearchLinkDelegate _createAdvanceSearchLink;
  4.  
  5. public PeopleSearchBoxExAdv() : base()
  6. {
  7.     Type searchBoxExType = typeof(SearchBoxEx);
  8.     Type[] parameterTypes = { typeof(TableRow), typeof(String), typeof(int).MakeByRefType() };
  9.     MethodInfo mi_CreateAdvanceSearchLink = searchBoxExType.GetMethod("CreateAdvanceSearchLink",
  10.             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, parameterTypes, null);
  11.     if (mi_CreateAdvanceSearchLink != null)
  12.     {
  13.         DynamicMethod dm = new DynamicMethod("CreateAdvanceSearchLink", null,
  14.             new Type[] { typeof(PeopleSearchBoxEx), typeof(TableRow), typeof(String), typeof(int).MakeByRefType() },
  15.             typeof(PeopleSearchBoxEx));
  16.         ILGenerator gen = dm.GetILGenerator();
  17.         gen.Emit(OpCodes.Ldarg_0);
  18.         gen.Emit(OpCodes.Ldarg_1);
  19.         gen.Emit(OpCodes.Ldarg_2);
  20.         gen.Emit(OpCodes.Ldarg_3);
  21.         gen.Emit(OpCodes.Call, mi_CreateAdvanceSearchLink);
  22.         gen.Emit(OpCodes.Ret);
  23.         _createAdvanceSearchLink =
  24.             (CreateAdvanceSearchLinkDelegate)dm.CreateDelegate(typeof(CreateAdvanceSearchLinkDelegate));
  25.     }

Having this done, calling the original version of the CreateAdvanceSearchLink method is so simple as this:

  1. protected void CreateAdvanceSearchLink(TableRow row, string keyword, ref int colsRest)
  2. {
  3.     if (_createAdvanceSearchLink != null)
  4.     {
  5.         _createAdvanceSearchLink((PeopleSearchBoxEx)this, row, keyword, ref colsRest);
  6.     }
  7. }

The rest of the code is about implementing the addition or merging the link as requested. If there are other links to display (like Preferences or Search Options) the we have to merge the Advanced link with these ones, otherwise, we have to add the new cell as is.

We have to handle the search keyword that we need to pass to the CreateAdvanceSearchLink method. The keyword itself is accessible through the protected field m_strKSFromPostOrGetOverride of the base class. In the SearchBoxEx implementation of the method it is trimmed using the internal static TrimAndChopStringBySize method of the internal SearchCommon class. Since it is a quite simple method and I did not want to involve more reflection into the code, I simple borrowed the method into my class:

  1. string TrimAndChopStringBySize(string strLongString, int iMaxSize)
  2. {
  3.     if (strLongString == null)
  4.     {
  5.         return string.Empty;
  6.     }
  7.     string str = strLongString.Trim();
  8.     int length = str.Length;
  9.     if (iMaxSize >= length)
  10.     {
  11.         return str;
  12.     }
  13.     if (iMaxSize > 3)
  14.     {
  15.         return (str.Substring(0, iMaxSize – 3) + "…");
  16.     }
  17.     return str.Substring(0, iMaxSize);
  18. }

If we add the Advanced link to a new cell of the row, and there is a second row in the web part for the Additional query description label, then we have to ensure that the column span of its cell is increased by one.

Based on the above, the CreateChildControls method of our class is as follows:

  1. protected override void CreateChildControls()
  2. {
  3.     base.CreateChildControls();
  4.  
  5.     // we have to alter the content only if advanced search link
  6.     // must be shown
  7.     if (ShowAdvancedSearch)
  8.     {
  9.         // table might be at first or later position
  10.         // depending on web part settings
  11.         Table table = null;
  12.         foreach (Control control in Controls)
  13.         {
  14.             if (control is Table)
  15.             {
  16.                 table = (Table)control;
  17.                 break;
  18.             }
  19.         }
  20.  
  21.         // table found
  22.         if (table != null)
  23.         {
  24.             //string str5 = SearchCommon.TrimAndChopStringBySize(this.m_strKSFromPostOrGetOverride, 200 – ((this._AppQueryTerms != null) ? this._AppQueryTerms.Length : 0));
  25.             String keyword = TrimAndChopStringBySize(this.m_strKSFromPostOrGetOverride, 200 – ((AppQueryTerms != null) ? AppQueryTerms.Length : 0));
  26.             //String keyword = Page.Request.QueryString["k"];
  27.  
  28.             // should be always true, but check to be sure…
  29.             if (table.Rows.Count > 0)
  30.             {
  31.                 int colsRest;
  32.                 // if either preferences or options are shown
  33.                 // we have to merge the advanced search link
  34.                 // into the existing cell
  35.                 if ((ShowPerferenceLink) || (ShowSearchOptions))
  36.                 {
  37.                     TableRow tr = new TableRow();
  38.                     colsRest = 0;
  39.  
  40.                     CreateAdvanceSearchLink(tr, keyword, ref colsRest);
  41.  
  42.                     int itemNum = (ShowPerferenceLink) ? 1 : 0;
  43.  
  44.                     // should be always true, but check to be sure…
  45.                     if ((tr.Cells.Count > 0) && (tr.Cells[0].Controls.Count > itemNum) &&
  46.                         (tr.Cells[0].Controls[itemNum] is HtmlGenericControl) &&
  47.                         (table.Rows[0].Cells.Count > 1))
  48.                     {
  49.                         // copy the 'entire DIV' tag, not the HyperLink only
  50.                         // into the next to last cell position
  51.                         HtmlGenericControl gc = (HtmlGenericControl)tr.Cells[0].Controls[itemNum];
  52.                         table.Rows[0].Cells[table.Rows[0].Cells.Count – 2].Controls.Add(gc);
  53.                     }
  54.  
  55.                 }
  56.                 // if neither preferences nor options are shown
  57.                 // we can add the new cell to the end of
  58.                 // of the existing row
  59.                 else
  60.                 {
  61.                     colsRest = table.Rows[0].Cells.Count; ;
  62.                     CreateAdvanceSearchLink(table.Rows[0], keyword, ref colsRest);
  63.  
  64.                     if (AppQueryTermsLabel != null)
  65.                     {
  66.                         // there must be a second line if the Additional query description label
  67.                         // is specified, but we check to be sure…
  68.                         if (table.Rows.Count > 1)
  69.                         {
  70.                             table.Rows[1].Cells[table.Rows[1].Cells.Count – 1].ColumnSpan++;
  71.                         }
  72.                     }
  73.                 }
  74.  
  75.             }
  76.  
  77.         }
  78.     }
  79. }

To test the code, we add the web part to the search center:

image

After setting the web part properties, including the ones that are responsible for the with of the web part and the search box, the result is as it should be out of the box:

image

You can download the sample from here.

October 17, 2010

Getting the type of the web part from a BinarySerializedWebPart

As I promised earlier, in this post I will show you a simple way to determine the type of the web part that is encoded in a BinarySerializedWebPart. The code in this post is only an extension to the code example in my former post.

The WebPart node of the BinarySerializedWebPart XML contains an attribute called WPTypeId that has a GUID value.

The internal WebPartTypeInfo class (Microsoft.SharePoint.WebPartPages namespace, Microsoft.SharePoint assembly) contains a static method, TryGetWellKnownTypeId, that provides the Type of the well known (built-in) web parts as an out parameter based on this GUID value.

The code below shows how to access this information using Reflection:

  1. XmlAttribute attribute = webPartNode.Attributes["WPTypeId"];
  2. if (attribute != null)
  3. {
  4.     Type webPartTypeInfoType = assembly.GetType("Microsoft.SharePoint.WebPartPages.WebPartTypeInfo");
  5.  
  6.     Guid wpTypeId = new Guid(attribute.Value);
  7.  
  8.     //internal static bool TryGetWellKnownTypeId(Guid webPartTypeId, out Type type);
  9.     MethodInfo mi_TryGetWellKnownTypeId = webPartTypeInfoType.GetMethod("TryGetWellKnownTypeId", BindingFlags.NonPublic | BindingFlags.Static);
  10.  
  11.     Type webPartType = null;
  12.  
  13.     object[] parameters = new object[2] { wpTypeId, webPartType };
  14.  
  15.     if ((bool)mi_TryGetWellKnownTypeId.Invoke(null, parameters))
  16.     {
  17.         webPartType = (Type)parameters[1];
  18.         Console.WriteLine("Web part type is: {0}", webPartType);
  19.     }
  20. }

You can use the result as the type parameter when calling the constructor of the BinaryWebPartDeserializer.

Decoding the content of the BinarySerializedWebPart – The code

Note: this is the second part of a former post. If you happen to have questions, please check the first part (that is about the theory) before asking something you may find the answer for in the first part.

Well, after so much theory I feel it is time to write some code for decoding of BinarySerializedWebPart.

Let’s see my former sample XML:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <Module Name="FileSystemForms" Url="Lists/Files" RootWebOnly="FALSE" SetupPath="pages">
  4.     <File Url="DispForm.aspx" Type="Ghostable" Path="form.aspx">
  5.       <BinarySerializedWebPart>
  6.         <GUIDMap>
  7.           <GUID Id="33ff2881_489d_4ce2_ac94_e81d64689d2a" ListUrl="Lists/Files" />
  8.         </GUIDMap>
  9.         <WebPart ID="{035cec7d-5f69-4dbf-a551-0b8203467c41}" WebPartIdProperty="" List="{$ListId:Lists/Files;}" Type="4"
  10.           Flags="0" DisplayName="" Version="4" Url="Lists/Files/DispForm.aspx" WebPartOrder="1" WebPartZoneID="Main"
  11.           IsIncluded="True" FrameState="0" WPTypeId="{feaafd58-2dc9-e199-be37-d6cdd7f84690}"
  12.           SolutionId="{00000000-0000-0000-0000-000000000000}" Assembly="" Class="" Src=""
  13.           AllUsers="B6Dt/kMAAAABAAAAAAAAAAIAAAAvX2xheW91dHMvaW1hZ2VzL2l0ZWJsLnBuZwAvRjFTaXRlL0xpc3RzL0ZpbGVzAP8BFCsAJQICAgMCAwEEAAICAhICFAEBAAIEBQtDb250cm9sTW9kZQspiAFNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scy5TUENvbnRyb2xNb2RlLCBNaWNyb3NvZnQuU2hhcmVQb2ludCwgVmVyc2lvbj0xNC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj03MWU5YmNlMTExZTk0MjljAQUIRm9ybVR5cGUCBAEAAAIWAoYBCyo0U3lzdGVtLldlYi5VSS5XZWJDb250cm9scy5XZWJQYXJ0cy5XZWJQYXJ0RXhwb3J0TW9kZQICggEFGi9fbGF5b3V0cy9pbWFnZXMvaXRlYmwucG5nAn0FEy9GMVNpdGUvTGlzdHMvRmlsZXMFCFBhZ2VUeXBlCyl3TWljcm9zb2Z0LlNoYXJlUG9pbnQuUEFHRVRZUEUsIE1pY3Jvc29mdC5TaGFyZVBvaW50LCBWZXJzaW9uPTE0LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTcxZTliY2UxMTFlOTQyOWMEBQdMaXN0VXJsZQUGTGlzdElkKClYU3lzdGVtLkd1aWQsIG1zY29ybGliLCBWZXJzaW9uPTIuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OSQzM2ZmMjg4MS00ODlkLTRjZTItYWM5NC1lODFkNjQ2ODlkMmEFD0xpc3REaXNwbGF5TmFtZWUClQEFJnszM0ZGMjg4MS00ODlELTRDRTItQUM5NC1FODFENjQ2ODlEMkF9BQ1YbWxEZWZpbml0aW9uBcUPDQo8VXNlckNvbnRyb2wgeDpDbGFzcz0iRm9ybVhtbFRvWGFtbC5Vc2VyQ29udHJvbDIiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIiB4bWxuczpTaGFyZVBvaW50PSJNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scyIgeG1sbnM6c3lzdGVtPSJjbHItbmFtZXNwYWNlOlN5c3RlbTthc3NlbWJseT1tc2NvcmxpYiI+PFN0YWNrUGFuZWwgeDpOYW1lPSJGb3JtIj4NCjxTdGFja1BhbmVsLlJlc291cmNlcz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtTW9kZSI+RGlzcGxheTwvc3lzdGVtOlN0cmluZz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtVHlwZSI+TGlzdEZvcm08L3N5c3RlbTpTdHJpbmc+DQo8L1N0YWNrUGFuZWwuUmVzb3VyY2VzPg0KPFN0YWNrUGFuZWwgeDpOYW1lPSJNYWluU2VjdGlvbnMiPjxHcmlkPjxHcmlkLkNvbHVtbkRlZmluaXRpb25zPg0KPENvbHVtbkRlZmluaXRpb24gU3R5bGU9IntTdGF0aWNSZXNvdXJjZSBtcy1mb3JtbGFiZWx9Ii8+DQo8Q29sdW1uRGVmaW5pdGlvbiBTdHlsZT0ie1N0YXRpY1Jlc291cmNlIG1zLWZvcm1ib2R5fSIvPg0KPC9HcmlkLkNvbHVtbkRlZmluaXRpb25zPjxHcmlkLlJvd0RlZmluaXRpb25zPg0KPFJvd0RlZmluaXRpb24gLz4NCjxSb3dEZWZpbml0aW9uIC8+DQo8Um93RGVmaW5pdGlvbiAvPg0KPFJvd0RlZmluaXRpb24gLz4NCjwvR3JpZC5Sb3dEZWZpbml0aW9ucz4NCjxTaGFyZVBvaW50OkZpZWxkTGFiZWwgR3JpZC5Db2x1bW49IjAiIEdyaWQuUm93PSIwIiBDb250cm9sTW9kZT0iRGlzcGxheSIgRmllbGROYW1lPSJOYW1lIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJOYW1lIiBGaWVsZEludGVybmFsTmFtZT0iTmFtZSIgRmllbGRUeXBlPSJUZXh0IiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMCIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTmFtZSIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMSIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iU2l6ZSIgLz4NCjxDb21tZW50IEZpZWxkTmFtZT0iU2l6ZSIgRmllbGRJbnRlcm5hbE5hbWU9IlNpemUiIEZpZWxkVHlwZT0iSW50ZWdlciIgLz4NCjxTaGFyZVBvaW50OkZvcm1GaWVsZCBHcmlkLkNvbHVtbj0iMSIgR3JpZC5Sb3c9IjEiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IlNpemUiIEluY2x1ZGVEZXNjcmlwdGlvbj0iVHJ1ZSIvPg0KPFNoYXJlUG9pbnQ6RmllbGRMYWJlbCBHcmlkLkNvbHVtbj0iMCIgR3JpZC5Sb3c9IjIiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IkNyZWF0ZWQiIC8+DQo8Q29tbWVudCBGaWVsZE5hbWU9IkNyZWF0ZWQiIEZpZWxkSW50ZXJuYWxOYW1lPSJDcmVhdGVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMiIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iQ3JlYXRlZCIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJMYXN0IG1vZGlmaWVkIiBGaWVsZEludGVybmFsTmFtZT0iTGFzdE1vZGlmaWVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiBJbmNsdWRlRGVzY3JpcHRpb249IlRydWUiLz4NCjwvR3JpZD4NCjwvU3RhY2tQYW5lbD4NCjwvU3RhY2tQYW5lbD4NCjwvVXNlckNvbnRyb2w+AktkBRFQYXJhbWV0ZXJCaW5kaW5ncwXoAg0KPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iZHZ0X2Fwb3MiIExvY2F0aW9uPSJQb3N0YmFjaztDb25uZWN0aW9uIi8+DQogICAgICAgIDxQYXJhbWV0ZXJCaW5kaW5nIE5hbWU9IlVzZXJJRCIgTG9jYXRpb249IkNBTUxWYXJpYWJsZSIgRGVmYXVsdFZhbHVlPSJDdXJyZW50VXNlck5hbWUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iVG9kYXkiIExvY2F0aW9uPSJDQU1MVmFyaWFibGUiIERlZmF1bHRWYWx1ZT0iQ3VycmVudERhdGUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iTGlzdEl0ZW1JZCIgTG9jYXRpb249IlF1ZXJ5U3RyaW5nKElEKSIgRGVmYXVsdFZhbHVlPSIwIi8+DQogICAgICAgIA==" />
  14.       </BinarySerializedWebPart>
  15.       </File>
  16.     </Module>
  17. </Elements>

The DeserializeWebPart method below shows the decoding process of the encoded web part. It first read and Base64 decode the binary serialized attribute value, then instantiate the BinaryWebPartDeserializer via calling its protected constructor, and gets the web part by calling the internal Deserialize method. Note, that we get the SPWebPartManager we need also using Reflection, by reading the internal WebPartManager property of the SPWeb instance passed to the method as parameter. I assume you can use any SPWeb instance here, but it requires yet further testing.

In the XML in this example, there is only an AllUsers attribute. If your XML contains PerUser and/or View attributes, you should read those values too, and include them as parameters when calling the constructor of the BinaryWebPartDeserializer.

In this example I knew that the result must be an XsltListViewWebPart, so I used that fixed type in the code. If you are not sure in the web part type, I will show you a method in a following post how to get the type from the XML either.

  1. public WebPart DeserializeWebPart(SPWeb web, String xmlPath)
  2. {
  3.     
  4.     XmlDocument sampleXml = new XmlDocument();
  5.     sampleXml.Load(xmlPath);
  6.     XmlNamespaceManager nsmgr = new XmlNamespaceManager(sampleXml.NameTable);
  7.     nsmgr.AddNamespace("sp", "http://schemas.microsoft.com/sharepoint/&quot;);
  8.     XmlNode webPartNode = sampleXml.SelectSingleNode("sp:Elements/sp:Module/sp:File/sp:BinarySerializedWebPart/sp:WebPart", nsmgr);
  9.  
  10.     byte[] allUsers = GetBinarySerializedAttribute("AllUsers", webPartNode);
  11.     //byte[] perUser = GetBinarySerializedAttribute("PerUser", webPartNode);
  12.     //byte[] view = GetBinarySerializedAttribute("View", webPartNode);
  13.     
  14.     Assembly assembly = typeof(SPSite).Assembly;
  15.     
  16.     Type spWebPartManagerType = typeof(SPWebPartManager);
  17.     Type xmlNamespaceManagerType = typeof(XmlNamespaceManager);
  18.     Type spWebType = typeof(SPWeb);
  19.     Type typeType = typeof(Type);
  20.     Type stringArrayType = typeof(String).MakeArrayType();
  21.     Type byteArrayType = typeof(byte).MakeArrayType();
  22.     // or alternatively we could use
  23.     //Type byteArrayType = typeof(byte[]);
  24.     Type stringType = typeof(String);
  25.     Type xsltListViewWebPartType = typeof(XsltListViewWebPart);
  26.  
  27.     PropertyInfo pi_WebPartManager = spWebType.GetProperty("WebPartManager", BindingFlags.NonPublic | BindingFlags.Instance);
  28.     SPWebPartManager spWebPartManager = (SPWebPartManager)pi_WebPartManager.GetValue(web, null);
  29.  
  30.     Type binaryWebPartDeserializerType = assembly.GetType("Microsoft.SharePoint.WebPartPages.BinaryWebPartDeserializer");
  31.     // protected BinaryWebPartDeserializer(SPWebPartManager webPartManager, XmlNamespaceManager xmlnsManager, byte[] userData, byte[] sharedData, string webPartIdProperty, string[] links, Type type, SPWeb spWeb) : base(webPartManager)
  32.     ConstructorInfo ci_BinaryWebPartDeserializerType = binaryWebPartDeserializerType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,
  33.          null, new Type[8] { spWebPartManagerType, xmlNamespaceManagerType, byteArrayType, byteArrayType, stringType, stringArrayType, typeType, spWebType }, null);
  34.     object binaryWebPartDeserializer = ci_BinaryWebPartDeserializerType.Invoke(new object[8] { null, null, null, allUsers, null, null, xsltListViewWebPartType, null });
  35.     MethodInfo mi_Deserialize = binaryWebPartDeserializerType.GetMethod("Deserialize", BindingFlags.NonPublic | BindingFlags.Instance);
  36.     WebPart webPart = (WebPart)mi_Deserialize.Invoke(binaryWebPartDeserializer, null);
  37.  
  38.     return webPart;
  39. }

Note, that the BinarySerializedWebPart node might be found in other context either. See the following example:

  1. <View List="Shared Documents" DisplayName="" Url="" DefaultView="FALSE" BaseViewID="1" Type="HTML" WebPartOrder="0" WebPartZoneID="Left" ContentTypeID="0x" ID="g_ba709f71_6af5_4e3c_a8b1_01be2d3f95e8" Hidden="TRUE">
  2.   <BinarySerializedWebPart>
  3.     <GUIDMap>
  4.       <GUID Id="33ff2881_489d_4ce2_ac94_e81d64689d2a" ListUrl="Lists/Files" />
  5.     </GUIDMap>
  6.     <WebPart ID="{035cec7d-5f69-4dbf-a551-0b8203467c41}" WebPartIdProperty="" List="{$ListId:Lists/Files;}" Type="4"
  7.              Flags="0" DisplayName="" Version="4" Url="Lists/Files/DispForm.aspx" WebPartOrder="1" WebPartZoneID="Main"
  8.              IsIncluded="True" FrameState="0" WPTypeId="{feaafd58-2dc9-e199-be37-d6cdd7f84690}"
  9.              SolutionId="{00000000-0000-0000-0000-000000000000}" Assembly="" Class="" Src=""
  10.              AllUsers="B6Dt/kMAAAABAAAAAAAAAAIAAAAvX2xheW91dHMvaW1hZ2VzL2l0ZWJsLnBuZwAvRjFTaXRlL0xpc3RzL0ZpbGVzAP8BFCsAJQICAgMCAwEEAAICAhICFAEBAAIEBQtDb250cm9sTW9kZQspiAFNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scy5TUENvbnRyb2xNb2RlLCBNaWNyb3NvZnQuU2hhcmVQb2ludCwgVmVyc2lvbj0xNC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj03MWU5YmNlMTExZTk0MjljAQUIRm9ybVR5cGUCBAEAAAIWAoYBCyo0U3lzdGVtLldlYi5VSS5XZWJDb250cm9scy5XZWJQYXJ0cy5XZWJQYXJ0RXhwb3J0TW9kZQICggEFGi9fbGF5b3V0cy9pbWFnZXMvaXRlYmwucG5nAn0FEy9GMVNpdGUvTGlzdHMvRmlsZXMFCFBhZ2VUeXBlCyl3TWljcm9zb2Z0LlNoYXJlUG9pbnQuUEFHRVRZUEUsIE1pY3Jvc29mdC5TaGFyZVBvaW50LCBWZXJzaW9uPTE0LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTcxZTliY2UxMTFlOTQyOWMEBQdMaXN0VXJsZQUGTGlzdElkKClYU3lzdGVtLkd1aWQsIG1zY29ybGliLCBWZXJzaW9uPTIuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OSQzM2ZmMjg4MS00ODlkLTRjZTItYWM5NC1lODFkNjQ2ODlkMmEFD0xpc3REaXNwbGF5TmFtZWUClQEFJnszM0ZGMjg4MS00ODlELTRDRTItQUM5NC1FODFENjQ2ODlEMkF9BQ1YbWxEZWZpbml0aW9uBcUPDQo8VXNlckNvbnRyb2wgeDpDbGFzcz0iRm9ybVhtbFRvWGFtbC5Vc2VyQ29udHJvbDIiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIiB4bWxuczpTaGFyZVBvaW50PSJNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scyIgeG1sbnM6c3lzdGVtPSJjbHItbmFtZXNwYWNlOlN5c3RlbTthc3NlbWJseT1tc2NvcmxpYiI+PFN0YWNrUGFuZWwgeDpOYW1lPSJGb3JtIj4NCjxTdGFja1BhbmVsLlJlc291cmNlcz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtTW9kZSI+RGlzcGxheTwvc3lzdGVtOlN0cmluZz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtVHlwZSI+TGlzdEZvcm08L3N5c3RlbTpTdHJpbmc+DQo8L1N0YWNrUGFuZWwuUmVzb3VyY2VzPg0KPFN0YWNrUGFuZWwgeDpOYW1lPSJNYWluU2VjdGlvbnMiPjxHcmlkPjxHcmlkLkNvbHVtbkRlZmluaXRpb25zPg0KPENvbHVtbkRlZmluaXRpb24gU3R5bGU9IntTdGF0aWNSZXNvdXJjZSBtcy1mb3JtbGFiZWx9Ii8+DQo8Q29sdW1uRGVmaW5pdGlvbiBTdHlsZT0ie1N0YXRpY1Jlc291cmNlIG1zLWZvcm1ib2R5fSIvPg0KPC9HcmlkLkNvbHVtbkRlZmluaXRpb25zPjxHcmlkLlJvd0RlZmluaXRpb25zPg0KPFJvd0RlZmluaXRpb24gLz4NCjxSb3dEZWZpbml0aW9uIC8+DQo8Um93RGVmaW5pdGlvbiAvPg0KPFJvd0RlZmluaXRpb24gLz4NCjwvR3JpZC5Sb3dEZWZpbml0aW9ucz4NCjxTaGFyZVBvaW50OkZpZWxkTGFiZWwgR3JpZC5Db2x1bW49IjAiIEdyaWQuUm93PSIwIiBDb250cm9sTW9kZT0iRGlzcGxheSIgRmllbGROYW1lPSJOYW1lIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJOYW1lIiBGaWVsZEludGVybmFsTmFtZT0iTmFtZSIgRmllbGRUeXBlPSJUZXh0IiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMCIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTmFtZSIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMSIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iU2l6ZSIgLz4NCjxDb21tZW50IEZpZWxkTmFtZT0iU2l6ZSIgRmllbGRJbnRlcm5hbE5hbWU9IlNpemUiIEZpZWxkVHlwZT0iSW50ZWdlciIgLz4NCjxTaGFyZVBvaW50OkZvcm1GaWVsZCBHcmlkLkNvbHVtbj0iMSIgR3JpZC5Sb3c9IjEiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IlNpemUiIEluY2x1ZGVEZXNjcmlwdGlvbj0iVHJ1ZSIvPg0KPFNoYXJlUG9pbnQ6RmllbGRMYWJlbCBHcmlkLkNvbHVtbj0iMCIgR3JpZC5Sb3c9IjIiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IkNyZWF0ZWQiIC8+DQo8Q29tbWVudCBGaWVsZE5hbWU9IkNyZWF0ZWQiIEZpZWxkSW50ZXJuYWxOYW1lPSJDcmVhdGVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMiIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iQ3JlYXRlZCIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJMYXN0IG1vZGlmaWVkIiBGaWVsZEludGVybmFsTmFtZT0iTGFzdE1vZGlmaWVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiBJbmNsdWRlRGVzY3JpcHRpb249IlRydWUiLz4NCjwvR3JpZD4NCjwvU3RhY2tQYW5lbD4NCjwvU3RhY2tQYW5lbD4NCjwvVXNlckNvbnRyb2w+AktkBRFQYXJhbWV0ZXJCaW5kaW5ncwXoAg0KPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iZHZ0X2Fwb3MiIExvY2F0aW9uPSJQb3N0YmFjaztDb25uZWN0aW9uIi8+DQogICAgICAgIDxQYXJhbWV0ZXJCaW5kaW5nIE5hbWU9IlVzZXJJRCIgTG9jYXRpb249IkNBTUxWYXJpYWJsZSIgRGVmYXVsdFZhbHVlPSJDdXJyZW50VXNlck5hbWUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iVG9kYXkiIExvY2F0aW9uPSJDQU1MVmFyaWFibGUiIERlZmF1bHRWYWx1ZT0iQ3VycmVudERhdGUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iTGlzdEl0ZW1JZCIgTG9jYXRpb249IlF1ZXJ5U3RyaW5nKElEKSIgRGVmYXVsdFZhbHVlPSIwIi8+DQogICAgICAgIA==" />
  11.   </BinarySerializedWebPart>
  12. </View>

For this type of XML, you can get the node using the code:

  1. XmlDocument sampleXml = new XmlDocument();
  2. sampleXml.Load(xmlPath);
  3. XmlNode webPartNode = sampleXml.SelectSingleNode("View/BinarySerializedWebPart/WebPart");

No let’s see the helper methods I used. First there is the GetBinarySerializedAttribute method that reads and Base64 decodes the attribute value. It returns only the byte array that we can be used later in the Deserialize method of the ObjectStateFormatter class. So we have to look for the leading xFF x01 byte pattern in the decoded byte array. In this case I chose not the most sophisticated way of search: I simple convert the byte array to a hexadecimal representation and look for the position of the “-FF-01” substring. Since we have not to work with large streams and byte pattern, this algorithm is good / quick enough and provides hopefully more readable code than an arbitrary byte pattern finder implementation.

  1. private byte[] GetBinarySerializedAttribute(String attributeName, XmlNode webPartNode)
  2. {
  3.     byte[] decodedBytes = null;
  4.     XmlAttribute attribute = webPartNode.Attributes[attributeName];
  5.     if (attribute != null)
  6.     {
  7.         String serializedValue = attribute.Value;
  8.         decodedBytes = Convert.FromBase64String(serializedValue);
  9.     }
  10.  
  11.     String fullHexString = ByteArrayToString(decodedBytes);
  12.     int startPos = fullHexString.IndexOf("-FF-01");
  13.     String contentHexString = fullHexString.Substring(startPos);
  14.     byte[] result = StringToByteArray(contentHexString);
  15.  
  16.  
  17.     // this few lines of code only to test the result of deserialization
  18.     // you can remove it
  19.     ObjectStateFormatter formatter = new ObjectStateFormatter();
  20.     if ((result != null) && (result.Length != 0))
  21.     {
  22.         Object deserialized = formatter.Deserialize(new MemoryStream(result));
  23.     }
  24.     // end of testing block
  25.  
  26.     return result;
  27. }

Part of this method is only to test what type of object the deserialization will result. In my experience it is a simple object array as shown here:

image

Further helper methods for byte array – string conversions:

  1. public byte[] StringToByteArray(String hex)
  2. {
  3.     int NumberChars = hex.Length;
  4.     byte[] bytes = new byte[NumberChars / 3];
  5.     for (int i = 0; i < NumberChars; i += 3)
  6.         bytes[i / 3] = Convert.ToByte(hex.Substring(i + 1, 2), 16);
  7.     return bytes;
  8. }
  9.  
  10. private string ByteArrayToString(byte[] ba)
  11. {
  12.     string hex = BitConverter.ToString(ba);
  13.     // append a leading – to the hex string to ensure each byte is 3 chars
  14.     return "-" + hex;
  15. }

Hopefully the code runs successfully and return the deserialized WebPart. From this object you can get the values you need either writing out the necessary properties or simply via checking the properties in Visual Studio IDE.

image

September 12, 2009

Customizing the I need to… web part

Filed under: SharePoint, Web part — Tags: , — Peter Holpar @ 03:30

Recently I got a question about how one can customize the I need to… web part. The green button has to be removed and on selecting an item from the drop down list, the browser should navigate to the URL that is assigned to the selected item.

After some investigation I found a solution for the request, that I would like to share here.

First we need to delete all instances of this web part from the page. Then add a new one, but make no customization on it, instead export the web part.

To do that:

  • From the Site Actions menu select Edit Page.
  • From the edit drop down of the I need to… web part select Export…
  • Save the file to the file system.

Open the exported file in a text editor, like Notepad.

Modify line 76 (add the content in bold):

&lt;select id="TasksAndToolsDDID" class="ms-selwidth" 
style="width:{$tasksAndTools_Width}" size="1" title="Choose a task that you need
to perform"
onchange="TATWP_jumpMenu()" &gt;

Modify line 84 (add the content in bold):

&lt;select id="TasksAndToolsDDID" class="ms-selwidth" size="1" title="Choose 
a task that you need to perform"
onchange="TATWP_jumpMenu()"
&gt;

Delete lines between 92 (&lt;xsl:if test="$tasksAndtools_IsRTL = true()"&gt;) and 105 (&lt;/xsl:if&gt;).

Save the file.

Delete the I need to… web part from your page again. Import the web part from the modified file to the page. If you don’t know how to do that, here it is step-by-step:

  • From the Site Actions menu select Edit Page.
  • Click the Add a Web Part label in the web part zone you want to add the new web part to.
  • In the bottom right hand corner of the dialog click Advanced Web Part gallery and options.
  • There is a drop down arrow in the top right hand corner of the task pane next to to Browse label.  From this drop down select the third option: Import.
  • Browse to the modified file in the file system and click Upload and next click Import at the bottom of the page.

Configure the new web part as needed, press OK, then refresh the page.

If you don’t know how to configure this web part, see this post in Nicholas Bisciotti’s Blog.

The result of the customization is shown below:

image_thumb2

When selecting an item from the drop down, the browser is navigate to the selected target, as required.

Blog at WordPress.com.