Pages

November 29, 2016

How To Show Solution Name For Each Feature In SharePoint





The objective:
Ever occurred to you that you loose track of what solutions relates to what features enabled at site or site collection level? Yeah I've been in that situation too.
The objective is to add new field to Site features & Site Collection features page (/_layouts/ManageFeatures.aspx) showing a link of Solution name for each feature.

Introduction:
First, let's have a look for on urls of different pages:
Site features:         /_layouts/ManageFeatures.aspx
Site Coll. features: /_layouts/ManageFeatures.aspx?Scope=Site
WebApp features: /_admin/ManageWebAppFeatures.aspx?WebApplicationId={Id}&IsDlg=1
 (from CA -> select WebApp -> Manage Features)
Farm features:       /_admin/ManageFarmFeatures.aspx
 (from CA -> System Settings -> Manage farm features)

In this post, I am going to customize 'ManageFeatures.aspx' page only. However, the same procedure below can be applied to other pages. (Note: influenced by code from following post , I decided that I should start customizing my solution http://blogs.msdn.com/b/varun_malhotra/archive/2012/01/31/sharepoint-2010-get-solution-file-name-from-feature-activated.aspx).


OOB Page Details:
Let's first understand contents of ManageFeatures.aspx page and how it works:


- Fields: FeatureIcon(image), FeatureTitleDescription(Text), FeatureActiveButton(button), FeatureActiveState(text)


- Page calls a user control (~/_controltemplates/FeatureActivator.ascx) to render the table of features
<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
 <wssuc:FeatureActivator runat="server" ID="featact" />
 <SharePoint:FormDigest runat=server />
</asp:Content>


- Page also calls another control ( ~/_controltemplates/FeatureActivatorItem.ascx) to do the feature logic
<AlternatingItemTemplate>
  <wssuc:FeatureActivatorItem
   DataItem='<%# Container.DataItem %>'
   AlternatingItem="true"
   runat="server"
/>


The feature logic is at code files:
- Microsoft.SharePoint.WebControls.FeatureActivator.cs
- Microsoft.SharePoint.WebControls.FeatureActivatorItem.cs


Customization Steps for SP2010 & SP2013:

1. First, make sure you backup the original manageFeatures.aspx page & user controls files (FeatureActivator.ascx & FeatureActivatorItem.ascx).


2. Make a copy for the following: page & user controls, then rename all with postfix with "Ex":
 ManageFeaturesEx.aspx, FeatureActivatorEx.ascx, FeatureActivatorItemEx.ascx

3. Do the following changes to edit 3 files:

- In ManageFeaturesEx.aspx file:
 Set the Src parameter to refer to FeatureActivatorEx.ascx
 <%@ Register TagPrefix="wssuc" TagName="FeatureActivator" src="~/_controltemplates/FeatureActivatorEx.ascx" %>

- In FeatureActivator.ascx file:

    1. Set the Src parameter to refer to FeatureActivatoritemEx.ascx
    <%@ Register TagPrefix="wssuc" TagName="FeatureActivatorItem" src="~/_controltemplates/FeatureActivatorItemEx.ascx"     %>

    2. Add new column header ("Solution") under <colgroup> add: <col width="10%" />

    3. Add new column content:
    *** For SP2010 ***
    Under <tr> add:       
      <th scope="col" id="FeatureSolution" class="ms-vh2" style="padding-bottom: 4px"><SharePoint:EncodedLiteral             runat="server" text="From Solution" EncodeMethod='HtmlEncode'/></th>

    *** For SP2013 ***
    Under <tr> add:       
      <th scope="col" id="FeatureSolution" class="ms-vh2-nofilter" style="padding-bottom: 4px"><SharePoint:EncodedLiteral         runat="server" text="From Solution" EncodeMethod='HtmlEncode'/></th>

- In FeatureActivatorItem.ascx file:
    1. Only for SP2013: First Add these 2 lines to import SharePoint namespaces
    <%@ Import Namespace="Microsoft.SharePoint" %>
    <%@ Import Namespace="Microsoft.SharePoint.Administration" %>

    2. insert the following code in page after the header tags & before first <td> tag:
    <script runat = "server">
        SPSite Site = SPContext.Current.Site;
        SPSolutionCollection FarmSolutions = SPContext.Current.Site.WebApplication.Farm.Solutions;
       
        private string getSolutionNameFromFeatureDef(SPFeatureDefinition featDef)
        {
        string result = string.Empty;
        Guid FeatureSolutionID = featDef.SolutionId;
        if (FeatureSolutionID != Guid.Empty)
        {
            SPSolution solution = FarmSolutions[FeatureSolutionID];
            if (solution != null)
            {
                result = solution.SolutionFile.DisplayName;
            }
        }
        return (result);
        }

        public string getSolutionName()
        {
        string result = string.Empty;
        try
        {
            Guid FeatureID = new Guid(this.FeatureId);
            SPFeatureDefinition CurrentFeatureDefinition;
            SPFeature CurrentFeature = Site.Features[FeatureID];
            if (CurrentFeature != null)
            {
                // feature is activated at site collection
                CurrentFeatureDefinition = CurrentFeature.Definition;
            }
            else
            {
                // feature is activated at Farm or other level
                CurrentFeatureDefinition = SPFarm.Local.FeatureDefinitions[FeatureID];
            }
            result = getSolutionNameFromFeatureDef(CurrentFeatureDefinition);
        }
        catch (System.Exception)
        {
            // do nothing when exception
        }       
        return (result);
       }
    </script>

    3. Then add a new <td> cell at the end of file:
      <td class="<% Response.Write(this.CssClass); %>" style="padding-top: 4px; padding-bottom: 4px; ">
            <table width="100%" cellpadding="0" cellspacing="0" border="0">
            <tr><td class="ms-vb2"><% Response.Write(getSolutionName());%></td></tr>
            </table>
    </td>


    Note: for SP2013 make sure to postfix any '_layouts/' with '15' to target the new hive


4. Copy the 3 files to hive (14 for SP2010 or 15 for SP2013) in all related WFE servers:
 - Copy controls FeatureActivatorEx.ascx, FeatureActivatorItemEx.ascx to \TEMPLATE\CONTROLTEMPLATES folder
- Copy page ManageFeaturesEx.aspx to \TEMPLATE\LAYOUTS folder

That's it! no need for IIS reset. Now you can enjoy having the solution info across all sites collection & site features across the farm.

Note: the solutions column will be empty when no wsp found for target feature (example: for OOB MS features that were stapled in with site definition). Also, solutions column will not support user solutions (sandboxed solutions).



September 19, 2016

Quick notes on debugging code workflows in SharePoint

Scenario:
You have a custom workflow in SharePoint 2010 or 2013, where it reads some configuration keys from web.config then continue to do some business logic.

What's the issue?
So you added the 2 keys to your web application web.config on your WFE server. The workflow worked just fine a couple of time, and was able to read the config keys, then the next time it failed to read one or both of the keys! The occurrence is sporadic and its kind of hit or miss!
2 cases from ULS: first time it fails to find key1, then next time it fails to find key2!

Let's debug:
First things first! Workflows run by both w3wp & owstimer processes.
In SharePoint, workflow always start running by the World Wide Web Publishing service (w3wp.exe). If it goes to sleep (by a pause or delay activities or excessive load), then the 'SPTimerV4' SharePoint Foundation Workflow Timer service (owstimer.exe) will wake it up and run it.
SharePoint will automatically decide which Workflow Timer service will run the workflow. The operation can occur on any server running the "Microsoft SharePoint Foundation Workflow Timer Service" service.

To debug at run time, attach VS 2010 to the w3wp and owstimer process to detect which one it is.

After deploying new code the timer service must be restarted (iisreset will not suffice)
net stop sptimerv4
net start sptimerv4


Solution:
To avoid wasting time figuring out why it didn't read the config keys, let's use a method to tell us the file name and machine name the code was trying to read the config key. Use it like this:



string KeyValue = getConfigKeyValue("MyKey");

Here is the nice method:

private string getConfigKeyValue(string keyName)
        {
            string KeyVal = string.Empty;
            bool error = false;
            try
            {
                KeyVal = ConfigurationManager.AppSettings[keyName];
                if (KeyVal == null)
                    error = true;
            }
            catch (Exception)
            {
                error = true;
            }

            if (error)
            {
                KeyVal = string.Format("The key '{0}' was not found in file {1} in machine {2}",
                    keyName, AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, Environment.MachineName);
                throw new Exception(KeyVal);
            }
            return KeyVal;
        }


That way we can find and set the missing key easily.

Example 1:
System.Exception: The key 'key1' was not found in file D:\inetpub\WWWROOT\wss\VirtualDirectories\7001\web.config in machine DevServerWFE1

Example 2:
System.Exception: The key 'key1' was not found in file C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\BIN\OWSTIMER.EXE.Config in machine DevServerWFE1



June 6, 2016

DevExpress resources - Part 2: How resources work in details

This post is about understanding how DevExpress handles images resources using IIS.

Refer to Part 1 for the initial problem & requirements:

The Goal:
DevExpress API is a solid one. I thought it would be beneficial to understand how resources work inside-out. This knowledge will help you while troubleshooting issues with resource loading when you use DevExpress controls in your application.


Main Components:


The assembly file DevExpress.Web.dll adds handler DevExpress.Web.ASPxHttpHandlerModule at 'DX.Ashx' path to handle any http request that contains the request "dxr.axd?….".

To check from IIS:



To check from web.config:


<add type="DevExpress.Web.ASPxClasses.ASPxHttpHandlerModule, DevExpress.Web.v12.2, Version=12.2.13.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a" verb="GET" path="DX.ashx" validate="false" />



How it works:
Note: For simplicity I am only talking about single resource request here. Comma-separated multiple resources can be requested also.

1. When a DevExpress dll is loaded, it will reference the resources it holds. For example the DevExpress.Web.v12.2.dll contains this image file "Images.GridView.sprite.png". This resource among others was loaded into internal resource storage collection when all assemblies were loaded. Each resource will be added to a dictionary collection and to be given an index value equal to total resources added already. In this case, the image was added at resource index of 45.

2. User navigates to a page that has a DevExpress UI control (ex: ASPxClientGridView) which contains an image resource (the grouping '+' icons) among others.

https://{SiteUrl}/../SomePage.aspx
 

3. Internally, the back-end code of the control inserts some js code on the front-end to call the url of the intended resource image. So the '+' icon will actually be calling this url to get content:
 https://{SiteUrl}/DXR.axd?r=1_45-IZnRb


4. The request format is: DXR.axd?r={AssemblyNumber}_{ResourceIndex}-{DateToken}
Let's break it down:


  • DXR stands for "DevExpressResource"
  • AssemblyNumber: an integer value represents which dll has the required resource. Use below table to figure which dll should serve your resource. I used assemblies of version 12 but it should apply to other versions as well. Example: the required resource we used “r=1_45-IZnRb” has '1' as value of AssemblyNumber, so the required dll file is “DevExpress.Web.v12.2.dll”.
Assembly
Number
Assembly Dll
0
DevExpress.Web.ASPxThemes.v12.2.dll
1
DevExpress.Web.v12.2.dll
2
Not used
3
Not used
4
DevExpress.Web.ASPxHtmlEditor.v12.2.dll
5
DevExpress.Web.ASPxSpellChecker.v12.2.dll
6
DevExpress.Web.ASPxTreeList.v12.2.dll
7
DevExpress.Web.ASPxPivotGrid.v12.2.dll
8
DevExpress.Web.ASPxScheduler.v12.2.dll
9
DevExpress.XtraReports.v12.2.Web.dll
10
DevExpress.XtraCharts.v12.2.Web.dll
11
DevExpress.Web.ASPxGauges.v12.2.dll
12
DevExpress.PivotGrid.v12.2.Core.dll
13
DevExpress.ExpressApp.Web.v12.2.dll
14
DevExpress.Web.Mvc.v12.2.dll
15
DevExpress.XtraDashboard.v12.2.Web.dll
  • ResourceIndex:  a positive integer value represents the index of a specific resource inside assembly.

  • DateToken: a string value of Base64 representation of last modified date of the resource or the assembly file.
5. When a resource request received, the custom handler module will process http request get the required resource via a resource manager if the http request path ends with “Dxr.axd” string. Only the portion before ‘-‘ is considered, so only the “r=1_45” part is to be processed & resolved to get the corresponding resource image if available. The date token will not be part of this.


6. So the resource manager will pass values of ‘1’ which will resolve into full dll name ‘DevExpress.Web.v12.2, Version=12.2.13.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a’ Key And pass the number '45' as a resource number, then the dictionary value is "DevExpress.Web.Images.GridView.sprite.png"

7. Then it will get the binary content of that resource file & put it in http response to be displayed in the control in page


8. If you want to test the resource directly, you can type the url for it: 
https://{SiteUrl}/DXR.axd?r=1_45-IZnRb






Notes: 

  • All of these resource images will be saved as "DXR.png" when downloaded from IE.
  • I found this config key "DXResourcesPhysicalPath". If it has content then it can be used internally to get resource path.
  • Another key to compress binary resources before returning them in http response.                        <add key="DXEnableResourceCompression" value="true"/>
  • There are other config keys that play a role in data compression & increased performance:            <add key="DXEnableCallbackCompression" value="true"/>
    <add key="DXEnableResourceMerging" value="true"/>