Thursday, March 14, 2013

Searches, by an International User, producing no results

This will be quick.

So I had an issue with a user from Beijing performing searches against a site, but no search results would appear. After some investigation, ie a couple of late nights of virtual troubleshooting sessions with the user, I discovered that SharePoint search was interpreting the query by way of the local language settings. Given that this user's local language was Mandarin, the English word searches he was performing did not make sense to SharePoint. To resolve his issue, it required me to adjust the default query language setup on the search results page. Here's how I did it:

I performed a search to navigate directly to the search results window. Next, the page was placed in edit mode. Under the "Search Core Results" webpart, I edited the properties. Expanding the "Results Query Options" section, I picked "English" as the default Query Language and saved my changes.

VoilĂ , it worked!

Wednesday, January 30, 2013

Universal PowerShell Script to Deploy SharePoint Solution Packages

So, I was tasked with deploying several solution packages to an SharePoint 2010 alpha environment. Obviously, task one was to gather all the packages to be deployed. Task two was going to turn into a tedious headache until I ventured off into the PowerShell scripting world. What resulted was a universal script that I could use going forward that would deploy any number of wsp files. Below follows the script that handles said deployment.

A couple of things to note:
   1. This script assumes that a ps1 script is generated and is included in the folder from which your WSPs will be deployed.
   2. You have access to a SharePoint server on the farm.


Set-ExecutionPolicy Unrestricted 


$fc = new-object -com scripting.filesystemobject
$curFolder = Split-Path ($MyInvocation.MyCommand.Path)
$folder = $fc.getfolder($curFolder)

foreach ($i in $folder.files) {
  IF ($i.name -like "*.wsp")
  {
    $fullPath = join-path $curFolder $i.name
    Add-SPSolution $fullPath
    Install-SPSolution -Identity $i.name -GACDeployment
  }
}


Write-host "SharePoint 2010 Solutions Have Been Deployed to the Farm"
Write-host "Review Central Administration For Accuracy"

Thursday, January 24, 2013

Extracting Solution Packages from SharePoint 2010 via PowerShell

There may come times when it becomes necessary to synchronize your SharePoint farms. In my experience, solution packages are virtual moving targets where developers apply changes willy-nilly. Thus, piecing together the contents of a solution package becomes a tedious and cumbersome task that distracts you from other time-sensitive projects.

Well, to speed up the process, you can use a shorthand approach by simply extracting the solution packages deployed to their respective content databases. How you ask? By way of Powershell. Here goes:
$solutions = [Microsoft.SharePoint.Administration.SPFarm]::Local.Solutions;

foreach ($solution in $solutions) {
        $solution.SolutionFile.SaveAs("c:\temp\packages\" + $solution.Name);
}

Friday, July 6, 2012

Parsing Managed Metadata fields in lists

Ok, here's an easy one (and short one for those of you short on patience). With the advent of the Managed Metadata Service, we've all come to know and love Groups, Term Sets and, naturally, Terms. So the question becomes, what is an effective way of parsing through a multi-value enabled Managed Metadata field to capture their text values. Easy, it's just a matter of parsing through a TaxonomyFieldValueCollection object. Here goes nothing...

First thing you have to remember is to apply a using directive to qualify use of the SharePoint Taxonomy class. I can't tell you enough how nice it is come across a post that reminds us what classes are leveraged for a given solution.

    using Microsoft.SharePoint.Taxonomy;

So let's say we have a ListItem declared as "_MyListItem," and we want to capture the text values saved in a Managed Metadata field called "MySpecialTerms" in a List object called "_MyListOfTerms." Here's how you do it:

List<string> _MyListOfTerms = new List<string>();

foreach (TaxonomyFieldValue aSingleTermValue in 
    (TaxonomyFieldValueCollection)_MyListItem["MySpecialTerms"])
{
   _MyListOfTerms.Add(aSingleTermValue.Label);
}

VoilĂ !

Happy Coding.

Wednesday, May 23, 2012

Popup Window After Event Receiver Fires

So here's a low tech approach to firing off a popup window after an event receiver fires. This approach makes the following assumptions:
  • Event occurs at list item level
  • Event Receiver cannot interrupt save process
  • Popup window loads after event receiver fires
  • Parent window loads list after save
So, what you need are the following objects:
  • Event receiver that inherits SPItemEventReceiver
  • Redirect Page
  • Popup page

Your first step is to create a new event receiver object. The event receiver will override the ItemAdded or ItemUpdated events. Bear in mind that these events are, by default, asynchronous events. Edit the elements.xml file for the event receiver to include a Synchronization node that specifies a synchronous action. Your XML should resemble the following:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Receivers ListTemplateId="100">
      <Receiver>
        <Name>MyProject_EventItemAdded</Name>
        <Type>ItemAdded</Type>
        <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
        <Class>MyProject_PopupEvent.MyProject_Event</Class>
        <SequenceNumber>10000</SequenceNumber>
        <Synchronization>Synchronous</Synchronization>
      </Receiver>
  </Receivers>
</Elements>

Next step is to include a constructor in the EventReceiver that initializes a variable to the Current HttpContext such as:

if (current == null)
                current = System.Web.HttpContext.Current;

Step 3 includes modifying the overridden ItemAdded or ItemUpdated events that perform a simple redirect call to the redirect page such as:

current.Response.Redirect({redirectUrlString});

Step 4: Your redirect page can be a simple page with inline code that captures any querystring values or anything that you want to pass on to the popup window. Otherwise, the redirect page will hold javascript code that generates the popup window such as:

 <script type='text/javascript'>
 <!-- 
     
     window.open("<%=_PopupURL %>", "PopupWindow", "location=0,status=0,scrollbars=0, width=550,height=650");

     if (window.location != window.parent.location) {
         window.frameElement.commitPopup();
     }
     else if (window.frameElement != null) {
        window.frameElement.waitDialog.close();
     }
     else {
         window.location = "<%=_SourceURL %>";
     }

// -->
</script>

Couple of things to note:
1. This javascript code accounts for whether the redirect page was called from a dialog page or a full page.
2. The _PopupURL and _SourceURL variables are derived from inline code on the redirect page
3. The redirect page either closes itself if it came from a dialog page or redirects to another page if it was called from a full page.

Happy coding.

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!