Wednesday, January 4, 2012

Overwriting the SP 2010 'Email Link' Ribbon button

And so it goes, our user community was looking for a way to email the content from a wiki page to users outside of the network. The solution was to overwrite the "E-Mail a Link" button found on the ribbon within the Page tab. But how do we accomplish this? Well, it turns out, it's pretty easy.

While we're not technically overwriting the functionality of the button, we are hiding it and generating our own. Thus, this task became a two step process (three if you want to add a configuration page for the email form):

Step one, hide the default "E-Mail a Link" button: To perform this, we need to write a feature that calls a Custom Action. I won't bore you with the specifics of how to generate a feature project and I'll cut to the juicy details on what needs to be written in the elements.xml file.

<CustomAction
    Id="RemoveEmailPageLinkRibbonButton"
    Location="CommandUI.Ribbon">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition
          Location="Ribbon.WikiPageTab.Share.EmailPageLink" />
      </CommandUIDefinitions>
    </CommandUIExtension>
  </CustomAction>


Step two, add a new Ribbon button to the Page tab: To perform this, we need to include in our Element.xml file another Custom Action. Again, I won't bore you with feature generation, so here's the code needed to add the button.

Few things to note about this code - I depended on Javascript (don't ask, I prefer it to jQuery). The clientside code here simply reads the current page and captures the content within the content area for the Wiki. Once that's established, I copy those values to variables in my modal dialog window, making them available to the child window.

<CustomAction
    Id="EmailContentButton"
    Location="CommandUI.Ribbon" 
    Sequence="5" 
    Title="Email">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.WikiPageTab.Share.Controls._children">
          <Button 
          Id="Ribbon.WikiPageTab.Share.EmailContentButton"
          Alt="E-Mail Page Content"
          Sequence="5"
          Command="EmailPageContent_Button"
          Image32by32="/_layouts/1033/images/formatmap32x32.png" Image32by32Top="-128" Image32by32Left="-448"
          Image16by16="/_layouts/1033/images/formatmap16x16.png" Image16by16Top="-16" Image16by16Left="-88"
          LabelText="E-Mail"
          TemplateAlias="o2" />
        </CommandUIDefinition>
      </CommandUIDefinitions>
      <CommandUIHandlers>
        <CommandUIHandler
        Command="EmailPageContent_Button"
        CommandAction="javascript: 
         function GetEmailContent()
          {
            var objMainDiv = document.getElementById('ctl00_PlaceHolderMain_WikiField');
            var params = new Object();
            var options;
            params.pageTitle = document.title;
            params.parentUrl = window.location.href;
            params.pageContent = '';
             
            // if regular wiki 
            if(objMainDiv != 'undefined' && objMainDiv != null)
            {
             var objTable = objMainDiv.getElementsByTagName('table');
             
             if(objTable[0] == 'undefined' || objTable[0] == null)
             {
                if(confirm('Could Not Detect Wiki Content.\nE-Mail Page Link?'))
                    location.href='mailto: ?subject='+document.title+'&body=' + escape(window.location.href);
                return;
              }
              var objDiv = objTable[0].getElementsByTagName('td');
              if(objDiv[0] == 'undefined' || objDiv[0] == null)
             {
                if(confirm('Could Not Detect Wiki Content.\nE-Mail Page Link?'))
                    location.href='mailto: ?subject='+document.title+'&body=' + escape(window.location.href);
              }
             
             for(var i=0;i<objDiv.length;i++)
             {
                if(objDiv[i] != 'undefined' && objDiv[i] != null)
                  params.pageContent += objDiv[i].childNodes[0].innerHTML;
             }
            }
            else
            {// else check for enterprise wiki 
              objMainDiv = document.getElementById('ctl00_PlaceHolderMain_PageContent__ControlWrapper_RichHtmlField');
              
              if(objMainDiv != 'undefined' && objMainDiv != null)
              {
                 params.pageContent += objMainDiv.innerHTML;
              }
              else
              {
                 if(confirm('This Wiki Has No Content.\nE-Mail Page Link?'))
                    location.href='mailto: ?subject='+document.title+'&body=' + escape(window.location.href);

                 return;
              }
            }
            
            options = { 
                url:'{SiteUrl}/_layouts/EmailWikiContent/EmailPageContent.aspx',
                title: document.title,
                width:550,
                height:650,
                args: params};
                SP.UI.ModalDialog.showModalDialog(options);
                }
                GetEmailContent();" />
              </CommandUIHandlers>
    </CommandUIExtension>
  </CustomAction>


Ok, so I hinted that there may be a step three involved. In my project, I included code to perform runtime configurations for the E-Mail form; such as enabling/disabling the subject and body fields. In any case, you have to add another Custom Action. Here goes, Step Three:

This code adds the link to my custom configuration Page to the Site Administration section of Site Settings.

<CustomAction
    Id="ModifyFeatureMailToProperty" 
    GroupId="SiteAdministration" 
    Location="Microsoft.SharePoint.SiteSettings" 
    Title="E-Mail Wiki Page Settings" 
    Rights="ManageWeb"
    Description="Adjust the Default E-Mail Settings for My Custom Wiki Page Content E-Mails">
    <UrlAction Url="_layouts/EmailWikiContent/configureCustomEmailForm.aspx" />
  </CustomAction>

Cheers!

Updating BizTalk Config with Powershell

So, I was tasked with performing a simple update to a BizTalk application. This update would read values from the BizTalk configuration and leverage said values to determine best course of action in processing messages. Why you ask. Because we wanted to create a configurable process that would eliminate the need to recompile our BizTalk solutions.

In any case, I was able to add a library to the project, from which the config values would be read. Now, the next step was in deploying the application and appending the config value to BTSNTSvc.exe.config and BTSNTSvc64.exe.config. Since I already leverage Powershell to perform my deployments, I ultimately decided to include scripts to perform the config updates. Below follows the code to add a setting within the AppSettings section, as well as a comment to differentiate the new config setting with all others:

The first step was in generating functions to perform the actual procedures for inserting appSettings nodes and including comments in the appSettings section.

function Set-ApplicationSetting ([string]$fileName, [string]$filepath, [string]$name, [string]$value)
{
    # Load the config file up in memory
    [xml]$a = get-content $fileName;

    # Find the app settings item to change
    if($a.configuration.appSettings.selectsinglenode("add[@key='" + $name + "']") -ne $null)
    {
 $a.configuration.appSettings.selectsinglenode("add[@key='" + $name + "']").value = $value
    }
    else
    {
 $newElem = $a.CreateElement("add")
 $newKeyAttr = $a.CreateAttribute("key")
 $newValAttr = $a.CreateAttribute("value")

 $newKeyAttr.innerText = $name
 $newValAttr.innerText = $value

 $newElem.Attributes.Append($newKeyAttr) > $null
 $newElem.Attributes.Append($newValAttr) > $null
 
 $a.configuration.appSettings.appendChild($newElem)
    }

    # Save Changes
    $a.Save($filepath + $filename)
}

function InsertAppSettingsComment ([string]$fileName, [string]$filepath, [string]$value)
{
    # Load the config file up in memory
    [xml]$a = get-content $fileName;

    $comment = $a.CreateComment($value)

    # Find the app settings item to change
    $a.configuration.appSettings.InsertBefore($comment, $a.configuration.appSettings.LastChild)

    # Save Changes
    $a.Save($filepath + $filename)
}


Next up was discovering the appropriate folder location of the BizTalk config file and assigning said location to a variable.
# Check for correct Program Files directory
if(Test-Path (${env:ProgramFiles(x86)} + "\Microsoft BizTalk Server 2009\BTSNTSvc.exe.config"))
{
 $BTSConfigPath = ${env:ProgramFiles(x86)} + "\Microsoft BizTalk Server 2009\"
}
else
{
 $BTSConfigPath = "D:\Program Files (x86)\Microsoft BizTalk Server 2009\"
}

# Set Program Files directory as current location
Set-Location $BTSConfigPath


Now, I want to include code to backup the current config file before applying any changes.
$currentDate = (get-date).tostring("mm_dd_yyyy-hh_mm_s") # month_day_year - hours_mins_seconds
$backup86 = ".\BTSNTSvc.exe_" + $currentDate + "_config.bak"
$backup64 = ".\BTSNTSvc64.exe_" + $currentDate + "_config.bak"

Write-Output "Backup Prior BTS Config files (86 & 64)"

Copy-Item ".\BTSNTSvc.exe.config" $backup86 -Force
Copy-Item ".\BTSNTSvc64.exe.config" $backup64 -Force


Next, I want to write the new appSettings node to the BizTalk config file.
Write-Output "Updating BizTalk Application Setting"

# Change the app setting for the path to the backups
Set-ApplicationSetting "BTSNTSvc.exe.config" $BTSConfigPath "My Node Key" "My Node Value"
Set-ApplicationSetting "BTSNTSvc64.exe.config" $BTSConfigPath "My Node Key" "My Node Value"


Finally, I want to add some comments to the appSettings section just above my new appSettings node.
# Insert Comments
InsertAppSettingsComment "BTSNTSvc.exe.config" $BTSConfigPath "My Config Setting"
InsertAppSettingsComment "BTSNTSvc64.exe.config" $BTSConfigPath "My Config Setting"


Great! My new appSettings node is now available in the BizTalk config file. But how do I remove those values if need-be? No worries, follow the below steps:

The first step, again, is to include functions that perform the actual process.

function Remove-ApplicationSetting ([string]$fileName, [string]$filepath, [string]$name, [string]$value)
{
    # Load the config file up in memory
    [xml]$a = get-content $fileName;

    # Find the app settings item to change
    if($a.configuration.appSettings.selectsinglenode("add[@key='" + $name + "']") -ne $null)
    {
 $killChild = $a.configuration.appSettings.selectsinglenode("add[@key='" + $name + "']")
 $a.configuration.appSettings.RemoveChild($killChild)
    
 # Save Changes
 $a.Save($filepath + $filename)
    }
}

function RemoveAppSettingsComment ([string]$fileName, [string]$filepath, [string]$value, [string]$siblingValue )
{
    # Load the config file up in memory
    [xml]$a = get-content $fileName;


    if($a.configuration.appSettings.selectsinglenode("add[@key='" + $siblingValue + "']") -ne $null)
    {
 $sibChild = $a.configuration.appSettings.selectsinglenode("add[@key='" + $siblingValue + "']")
 $killChild = $sibChild.PreviousSibling

 if($killChild.innerText -eq $value)
 {
  $a.configuration.appSettings.RemoveChild($killChild)
 }
    
 # Save Changes
 $a.Save($filepath + $filename)
    }
}


We'll leverage the same logic from earlier to determine the proper folder location of the BizTalk config file, so I won't revisit that here. Which leads to the next step, we'll remove the comments first
Write-Output "Removing Application Setting"
# Insert Comments
RemoveAppSettingsComment "BTSNTSvc.exe.config" $BTSConfigPath "My Config Setting" "My Node Key"
RemoveAppSettingsComment "BTSNTSvc64.exe.config" $BTSConfigPath "My Config Setting" "My Node Key"


Finally, we remove the appSettings node.
# Change the app setting for the path to the backups
Remove-ApplicationSetting "BTSNTSvc.exe.config" $BTSConfigPath "My Node Key"
Remove-ApplicationSetting "BTSNTSvc64.exe.config" $BTSConfigPath "My Node Key"


Cheers!