Sunday, January 31, 2016

Update Managed Metadata fields using Nintex Workflow Custom Action

Background

In one of my recent projects, we were building a Reusable Nintex Workflow and one of the steps was to update managed metadata field. To our surprise we found that even Nintex 2013 Workflow does not have any action to update the managed metadata. There are workaround by using webservice and Term Guid  but that was not an ideal solution for us. 

Some of our requirements

  • We want to update one or many managed metadata fields in the workflow. The value of the field will be determined by the outcomes of other workflow steps.
  • We cannot hardcode Term GUID in the workflow. We have automated ways to deploy our workflow (through scripts) and it goes through few environments (Dev, System Integration, UAT, Prod-Support) before it goes to production. In each environment the Term GUID will be different.
  • Easy to use. Same methodology will be used in different workflows in different projects. So it must be easy to use, intuitive option.
  • Storing user credential was not an option.

Solution: Nintex Workflow Custom Action

What can be more easier way than to drag an action into the workflow designer and configure the field name and Managed Metadata value?  So I develop a Nintex Workflow Custom Action, “Taxonomy Update”. Below I am providing the ready to use solution and instruction to use the action. Hope others might find it useful.

Deploying Nintex Workflow Custom Action (3 Steps)


  • Deploy the wsp in SharePoint farm.

    You can use the following Powershell commands to deploy the wsp.
    Add-SPSolution Cassini.Workflows.wsp
            Install-SPSolution –Identity Cassini.Workflows.wsp  -GACDeployment
  • Activate the WebApplication feature (Cassini.NW.CustomActions)

    Go to Central Admin > Application Management > Manage web applications > Select the Web application you want to use the Custom Action > Click Manage feature form the Ribbon > Activate Cassini.NW.CustomActions feature.

Using/Configuring Taxonomy Update Action


  • Create a new Nintex workflow or open an existing Nintex workflow. 
  • Add "Taxonomy Update"

    In the left ToolBox, you will see a new option “Cassini Custom Actions”. If you click that you will find the newly installed action “Taxonomy Update”. Drag the action into the canvas.
  • Configure

    Double click on the newly added action to configure. In the Taxonomy Field Name enter the name of the field/column in that document library/list that you want to update. In the Field Value enter the full path of the term separated by pipe (|).



    So if you are updating a managed metadata field called "Document Status", the value of Taxonomy Field Name will be Document Status.


    If you are setting the value to a term named "Approved" that resides in Taxonomy Term Store "Managed Metadata Service", Term Group "Cassini" and Term Set "Document Status", then the full path will be: "Managed Metadata Service|Cassini|Document Status|Approved".

    You can also select sub term e.g. "Managed Metadata Service|Cassini|Document Status|Finalised|Archived".




    Hope this helps!

Monday, February 28, 2011

Reading Content Control values programmatically

Content control is one of the cool features in Microsoft Word 2010 (first introduced in 2007). Imaging cases, where user submits their monthly/yearly report by MS Word file. Those files definitely contain quantitative & qualitative data that need to be entered into a main system, used by the company, for review and future planning. Generally there are data entry people who entered these values manually; but with very minimal effort we can automate the process.

We can give template MS Word file to those users which they will fill-up and submit each month/year. In the template file we will have content controls (textbox, drop down list etc). Programmatically we can extract and process these values very easily. And then we will enter these values into the main system without any delaying.

To accomplish this, first enable the developer tab: (Steps from MS):
  1. Start the application.
  2. Click the File tab.
  3. Click Options.
  4. In the categories pane, click Customize Ribbon.
  5. In the list of main tabs, select Developer.
  6. Click OK to close the Options dialog box.
Create your template by adding controls:
  1. Make sure you are at Developer tab (Marked as [1])
  2. You can drag and drop control from Ribbon (Marked as [2]) and set properties.
  3. Set a meaningful Title (Marked as [3]) which you can use in code.
  4. In case of drop down list add possible value (Marked as [4]).
  5. You can check the “Content control cannot be deleted” (Marked as [5]) to make sure user cannot delete the controls by mistake.

After this you can use the following code snippets to get all values from the doc file.

            WordprocessingDocument docC = WordprocessingDocument.Open("SampleSale.docx", false);
            MainDocumentPart mainPart = docC.MainDocumentPart;
            
            //will get the content controls that are part of a line
            List SdtRunList = mainPart.Document.Descendants().ToList();

            //will get the content controls that are paragrph
            List SdtBlockList = mainPart.Document.Descendants().ToList();
            
            for (Int32 i = 0; i < SdtRunList.Count; i++)
            {
                Console.WriteLine("(SdtRun) Title: " + SdtRunList[i].Descendants().First().Val.Value); //Printing title
                Console.Write("Value: ");                
                foreach (Text t in SdtRunList[i].Descendants())
                    Console.Write(t.InnerText); //Printing values

                Console.WriteLine(Environment.NewLine); // Adding some extra line
            }

            for (Int32 i = 0; i < SdtBlockList.Count; i++)
            {
                Console.WriteLine("(SdtBlock) Title: " + SdtBlockList[i].Descendants().First().Val.Value); //Printing title 
                Console.Write("Value: ");                
                foreach (Text t in SdtBlockList[i].Descendants())
                    Console.Write(t.InnerText); //Printing values
                
                Console.WriteLine(Environment.NewLine); // Adding some extra line
            }
           
            docC.Close();
            Console.ReadKey();
           //Ignore this line:  


Make sure you are getting both SdtRun and SdtBlock objects to read entire content controls. To understand the Open XML Object Model you can go through several articles but you must use “Open XML SDK 2.0 Productivity Tool” to explore the file by yourself. It will speed-up the learning process.
Here is a screenshot of the tools (exploring SampleSale.docx file) 


Feel free to leave any comment :)


Download
SampleSale.docx | Code.

Monday, October 26, 2009

FCKEditor in SharePoint (IE & Non-IE)

In SharePoint, we face one common problem while editing a Rich Text field from Non-IE browsers; the field does not have any Rich text editor. There are few blogs and forum posts showing how we can add FCKEditor in Non-IE browser. But after deployment we will no longer have consistent UI in IE and Non-IE browser for that screen. And after deployment, many users also find th FCKEditor more useful and easy than the default SharePoint Rich Text editor.

So in our project, we decided to use FCKEditor in both IE and Non-IE browser. To do this, I spent some time and came up with the following solution. However after deploying the solution we have tested well and we have not found any side effect, which is very nice :)



Steps:

You can download the FCKEditor controls from here. In our case we have unzipped the control in the following location:

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\CONTROLTEMPLATES\

We have to modify four JavaScript file:

1. FCKCONFIG.JS

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\CONTROLTEMPLATES\fckeditor\

Add the following configurations in the "fckconfig.js" file to add two new toolbar sets:
FCKConfig.ToolbarSets["RichTextEditorMinimal"] = [
['Source','-','NewPage','Preview','-','Templates'],
['Cut','Copy','Paste','PasteText','-','Print'],
['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'], 
'/',
['FontFormat','FontName','FontSize'],['Link','Unlink'],
'/',
['Bold','Italic','Underline','StrikeThrough'],
['OrderedList','UnorderedList','-','Outdent','Indent'],
['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
['TextColor','BGColor'],['SpecialChar'],
['FitWindow']
] ;


FCKConfig.ToolbarSets["RichTextEditor"] = [
['Source','-','NewPage','Preview','-','Templates'],
['Cut','Copy','Paste','PasteText','-','Print'],
['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'], 
'/',
['FontFormat','FontName','FontSize'],
'/',
['Bold','Italic','Underline','StrikeThrough'],
['OrderedList','UnorderedList','-','Outdent','Indent'],
['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
['Link','Unlink'], 
'/',
['TextColor','BGColor'],['Image','Table','Rule','Smiley','SpecialChar'],
['FitWindow']
] ;


Why two?

Multi lines of text has two Rich Text type. One is Rich text (Bold, italics, text alignment) and another is Enhanced rich text (Rich text with pictures, tables, and hyperlinks). In above toolbar set I have listed the appropriate ones, those are supported by the field type. However, you can remove controls, if you want.

2. CORE.JS
C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\1033\

Add the following JavaScript at the end of the "CORE.JS" file. While webpage loading, this JavaScript will replace all textarea with FCKEditor dynamically. This will enable FCKEditor control in both IE & Non-IE browser.

function SLAC_NonIERichText() {
    var rx = /RTE_ConvertTextAreaToRichEdit\("(\w+)"/;
    var as = document.getElementsByTagName("script");
    var oXML = new XMLHttpRequest();
    oXML.open('GET', '/_controltemplates/fckeditor/fckeditor.js', false);
    oXML.send('');
    eval(oXML.responseText);
    // alert("Loaded FCKEditor.js");
  
    for (i = 0; i < as.length; i++) 
    {
        var st = as[i].text;
        if (rx.test(st))
        {
            var ctlid = rx.exec(st)[1];
            var t = document.getElementById(ctlid);
            var f = t.form;
            var oFCKEditor = new FCKeditor(ctlid);
            // alert(ctlid);
            oFCKEditor.BasePath = "/_controltemplates/fckeditor/";
            // oFCKEditor.ToolbarSet = "XWEB";
            oFCKEditor.ToolbarSet = "RichTextEditorMinimal";

            if (st.indexOf('FullHtml') >= 0)
            {
              oFCKEditor.ToolbarSet = "RichTextEditor";
            } 
            oFCKEditor.Width = t.clientWidth;

            if (oFCKEditor.Width < 500)
            {
              oFCKEditor.Width = 500;
            } 
            oFCKEditor.Height = t.clientHeight + 100;

            if (oFCKEditor.Height < 400) 
            {
              oFCKEditor.Height = 400;
            }
            oFCKEditor.ReplaceTextarea();
        }
    }
}

_spBodyOnLoadFunctionNames.push('SLAC_NonIERichText');

3. BFORM.JS
C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\1033\

Search with RTE_ConvertTextAreaToRichEdit in the file "BFORM.JS". You will find two results. One of those is a function. Modify the function and put return; as the first statement of the function. This will prevent the function from being executed. The function will look like the following:
function RTE_ConvertTextAreaToRichEdit(
   strBaseElementID,
   fRestrictedMode,
   fAllowHyperlink,
   strDirection,
   strWebLocale,
   fSimpleTextOnly,
   fEditable,
   fUseDynamicHeightSizing,
   iMaxHeightSize,
   iMinHeightSize,
   strMode,
   urlWebRoot,
   strThemeUrl,
   strBodyClassName,
   fAllowRelativeLinks,
   strBaseUrl,
   fUseDynamicWidthSizing,
   iMaxWidthSize,
   iMinWidthSize,
   fEnforceAccessibilityMode,
   fPreserveScript,
   fHookUpEvents,
   fGenerateToolbar
   )
{ 
 return; //we need to add only this. 
 //keep rest of the code as it is.
 //.....
}

4. FORM.JS
C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\1033\

Follow the same steps as we did for file "FORM.JS". Search with RTE_ConvertTextAreaToRichEdit in the file "FORM.JS". You will find one result, a function. Modify the function and put return; as the first statement of the function.

NON_IE.JS (Caution!)
C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\1033\

We should not modify this file any more. To enable FCKEditor in only Non-IE, we need to modify this file. But now we are enabling FCKEditor for both IE and Non-IE. And for that we have modified CORE.JS, which is invoked for all browsers. So we no longer need to modify NON_IE.JS. Well, if you mistakenly modify this file then if you open the page with Non-IE browser, you will find two FCKEditor for each field :)

However, I have used JavaScript function SLAC_NonIERichText from this forum.

Sunday, May 25, 2008

Which control causes the PostBack

In the page life cycle the Click event, SelectedIndexChanged event and other events raised by the UI controls are called after Page_Load event. But in many case we need to know which control causes the post back in Page_Load/Init function. If we are working with dynamic controls or other complex UI then this information we often need.
To determine if a control causes the Postback or not we can use the following function:

private Boolean CheckIfTheControlCausesPostBack(Control ctr)
{
   String ctrID = this.Page.Request.Params["__EVENTTARGET"];
   if (ctrID != null && ctrID != "")
   {
      String newctrID = ctrID.Replace("$", "_");
      if (newctrID == ctr.ClientID)
      return true;
   }
   else if (ctr.GetType().ToString() == "System.Web.UI.WebControls.ImageButton" || ctr.GetType().ToString() == "System.Web.UI.WebControls.Button")
   {
      String controlID = ctr.ClientID;
      String controlIDX = ctr.ClientID + ".x"; 
      String controlIDY = ctr.ClientID + ".y";
      foreach (String key in this.Page.Request.Form.AllKeys)
      {
         String newKey = key.Replace("$", "_");
         if (newKey == controlID || newKey == controlIDX || newKey == controlIDY)
         {
            return true;
         }
      }
    }
    return false; 
}

Understanding Code:
In most cases, this.Page.Request.Params["__EVENTTARGET"] contains the ID of control that causes Postback. But for Button and ImageButton the property remains empty string. Rather the control ID can be found in this.Page.Request.Form.AllKeys collection. So we have to loop through the collection and find if the ID exists. Notice I am concatenating “.x” and “.y” with control ID. This is because if the control is ImageButton then in that collection it contains ControlID + “.x” and ControlID + “.y” rather than ControlID.

And I am also replacing “$” sign with “_”, because if the control contains in other container (Panel, UserControl etc) then the ClientID of that control is ContainerID1_ContainerID2_ControlID; whereas in the collection it contains ContainerID1$ContainerID2$ControlID. So we have to replace that symbol before comparing.

To know more about Page Life Cycle: ASP.NET Page Life Cycle Overview

Sunday, May 18, 2008

Sharepoint Object Browser

Introduction
This application is a SharePoint site object browser which shows all the contents of a SharePoint site in a tree structure. The main objective of this sample program is to show; how to use SharePoint object model. This sample code is a very easy view of SharePoint development. But to develop in SharePoint these are must a developer should know.

Background
There are lots of sample and help about SharePoint administration and customization. But there are very few samples and examples in SharePoint programming. At the beginning, we really had to struggle to develop WebPart and to understand how object model and other SharePoint stuff work. I am working in SharePoint project for nearly one year and I felt to share some of my experiences with others using different sample application. And this is my first example.

What this application does?
This is a SharePoint site object browser application. In the textbox, followed by label Site Address, enter a SharePoint site address and then press Go button. You will see all subsites and content of subsites will be generated in a tree form in the TreeView control. In that control, if you click on a tree node of type list then the list’s contents will be shown in the bottom DataGridView. In the Grid you will find all columns and rows of that particular list. Even some columns, you will see here, will not be visible from SharePoint site. You can also select a column from the DropDownList and can update value for all rows for that particular column. But as I told, you will be able to see some SharePoint internal columns, those values we can not update.

Download Code
I have published the article on CodeProject. So you can download the source code from there. Link.

Understanding the code
We can take SharePoint site instance (SPSite) from site’s URL. Then we can access that SPSite object’s properties.
uxBrowser.Nodes.Clear();
SPSite site = new SPSite(uxAddress.Text);
TreeNode rootNode = uxBrowser.Nodes.Add(site.Url);
uxBrowser.NodeMouseClick += new TreeNodeMouseClickEventHandler(uxBrowser_NodeMouseClick);
for (Int32 i = 0; i < site.AllWebs.Count; i++)
  {
     SPWeb web = site.AllWebs[i];
     TreeNode webNode = rootNode.Nodes.Add(web.Name);
     for (Int32 j = 0; j < web.Lists.Count; j++)
     {
        SPList list = web.Lists[j];
        TreeNode listNode = webNode.Nodes.Add(list.Title);
        listNode.Tag = list;
     }
  } 
Here you can see, fist we are creating an instance of SPSite from a string (site URL) then we are getting AllWebs which is a collection of SPWeb objects. After that we loop through all SPWeb objects and it contents and extracting its information. While doing so, we are also adding these objects’s name and reference (saving in Tag property) in the TreeView control to generate the tree.
Now if you click on list type tree node, it will take the list reference from the Tag property.
_CurrentSelectedList = ((SPList)e.Node.Tag);
Then Fields property of_CurrentSelectedList will return the column collection of that list and finally _CurrentSelectedList.Items will return all rows (items) in that list. To make the data more presentable, I have added all contents in a DataTable then Bind it to the DataGridView.
When the user clicks on list type tree node, I am also assigning the DataTable as Data Source of the DropDownList. Now the user can select a column and update the column’s value. But one problem may arise, while updating the list; if the user does not have permission to update then it will generate an error. This can be solved by impersonating the Sharepoint\system user by using the SPSecurity.RunWithElevatedPrivileges method. This is a technique to use Object Model with full privilege.
SPSecurity.RunWithElevatedPrivileges(delegate()
  {
     using (SPSite ElevatedsiteColl = new SPSite(siteAddress))
     {
       ElevatedsiteColl.AllowUnsafeUpdates = true;
       using (SPWeb ElevatedSite = ElevatedsiteColl.OpenWeb(webName))
       {
          ....
       }
     }
  });

In between this method, the code will execute with an Administrative (Sharepoint\system) privilege. So we will be able to do any kind of read/update/delete operation. In the code you may also have noticed that I am initiating SPSite, SPWeb (those are created repeatedly) in a using block. This is because SPSite and SPWeb is very resource consuming object. So we have to make sure the instance gets disposed after its use.
Conclusion
I keep the source as simple as possible. Still if someone feels that some areas needed to explain more, you are welcome to ask me.