Thursday, August 23, 2012

Default SSRS 2012 Parameters Pane to Collapsed in a PerformancePoint Reporting Services Web Part


Using PerformancePoint Reporting Services web parts on a dashboard is a great way to display information alongside Analytic Reports, in ways that aren’t supported by Analytic Reports (e.g. maps or scatter charts) or for data that doesn’t exist in your Analysis Services cubes. Offering visible parameters for your users to manipulate can additionally increase the value and interactivity of a report. However, when displaying a PerformancePoint Reporting Services web part with parameters, the parameters pane defaults to being expanded. Below is an example of a line chart that wouldn’t be easily possible using an Analytic Chart, with the parameters pane obscuring half of it.

Generally this is good (i.e. having the parameters pane defaulting to expanded), but if real estate is an issue (like in the image above), you may want that pane collapsed in its initial state. To work around this issue, we can use the NotifyBrowserOfAsyncUpdate event and some jQuery to locate a PerformancePoint Reporting Services web part and collapse the parameters pane, if it exists. Note, that this event is triggered for every PerformancePoint web part that is updated on a page.

function NotifyBrowserOfAsyncUpdate(elem) {
    // id of web part being updated
    var elemId = $(elem).prop('id');
    // find iframe sources that contain .rdl
    $('#' + elemId).find('iframe[src*="%2Erdl"]').attr('src', function(i, val) {
        return val.replace('&rv:ParamMode=Displayed&','&rv:ParamMode=Collapsed&')
    });
}

Now when your dashboard is displayed, the parameters pane will default to being collapsed, showing your chart, map or tablix in all of its glory.

Saturday, July 28, 2012

SharePoint Designer and the PerformancePoint Stack Selector’s CurrentSelection Property Error


For a project I'm currently working on, I needed some functionality that isn't (easily) supported by the dashboards that you can create with PerformancePoint Dashboard Designer. This includes adding non-PerformancePoint (custom and OOTB) web parts to the dashboards and making connections between PerformancePoint and non-PerformancePoint web parts. It is certainly  possible to add web parts and create connections after you save a dashboard in Dashboard Designer, but you would have to repeat the process anytime you needed to republish (i.e. save) your dashboard from PerformancePoint. The alternative is to use a web part page to create your dashboard, adding web parts and creating your connections through the browser.

Unfortunately, however, if you have to use SharePoint Designer 2010 for any of your dashboard customizations, you will run into some of the lack of support that it has for PerformancePoint. This posting has to do with one of the more minor annoyances.
When working with a web part page utilizing PerformancePoint web parts in SharePoint Designer 2010, you may notice the following error in the Design Page View after saving a page that contains a PerformancePoint Stack Selector:

Cannot create an object of type 'System.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' from its string representation '[, ]' for the 'CurrentSelection' property.

A similar error message is displayed in place of each Stack Selector on the page when it is viewed in a web browser.
A workaround is to remove the following attribute from each of the StackWebPart tags on the page and save again.

CurrentSelection="[, ]"
Unfortunately, as this is merely a stopgap fix, this has to be done every time you open, edit and save a web part page containing PerformancePoint Stack Selectors. Hopefully this is addressed in some future cumulative update (as a note, I am currently developing using the Feb 2012 CU (v14.0.6117.5002)).

Thursday, June 21, 2012

Setting a Default Selection on a PerformancePoint Scorecard


Depending on the design of your dashboard, you may want to set a default selection on a PerformancePoint Scorecard that is used as a filter for other web parts on a dashboard page.
As an example, consider a dashboard that contains a PerformancePoint Filter, Scorecard and an Analytic Report (Grid or Chart). The filter is used to filter the scorecard and the report. Additionally, the scorecard is used to filter the report based on a row member connection. The filter contains a list of manufacturers, the scorecard provides KPI for a list of products a manufacturer carries and the report displays the count of a selected manufacturer’s product sold by state. “Manufacturer 1” sells products A, B and C and “Manufacturer 2” sells products B, C and D.

One approach is to set the default of the Analytic Report to something that could make sense without a Scorecard filter being applied. By selecting default Members for the Analytic Report in Dashboard Designer, the dashboard defaults to displaying information, even without a scorecard selection. Depending on the complexity of your dashboard, the data, etc. maybe this is acceptable (this approach did not make sense for my particular dashboards).
Another approach is to select a Member (e.g. “Unknown” product that no manufacturer carries) that won’t produce any data in the Analytic Report. The resulting chart would look something like the following (note, I’m using a PerformancePoint Stack Selector, hence the web part title within a dropdown box):



Not great, but at least we aren’t misleading the dashboard user. I initially took this approach and went so far as to add a message in any PerformancePoint Reporting Services (i.e. SSRS report) web parts that were connected to the scorecard telling the user that they had to select a product (i.e. row) in the scorecard, if one wasn’t selected.
In the end, I wasn’t satisfied with this approach either, so I went Googling and stumbled across this question and response (http://stackoverflow.com/questions/3372565/performancepoint-sharepoint-2010-and-jquery) which turned up the NotifyBrowserOfAsyncUpdate event. This event is triggered for every PerformancePoint web part that is updated on a page. By using some jQuery and by identifying some PerformancePoint CSS classes, we can determine when the scorecard has been updated and whether or not we need to select a default row member by triggering a cell click event. I use the function below in an external script for use with multiple dashboards (I’ll leave it to you to streamline the jQuery as I have attempted to make it more understandable by adding comments and removing additional functionality).

function NotifyBrowserOfAsyncUpdate(elem) {
    var elemId, selector, rows, row;
    // id of web part being updated
    elemId = $(elem).prop('id');
    // .sctb is a PerformancePoint Scorecard class
    selector = '#' + elemId + " table.sctb";

    if ($(selector).length != 0) {
        // scorecard web part found! get rows that have a class that begins with "r-" (i.e. class|="r")
        rows = $(selector).find('tr[class|="r"]');
        // .scs is a PerformancePoint Scorecard’s selected row class
        if ($(rows).find('.scs').size() == 0) {
            row = $(rows)[0];
            // find head cell in first row and click
            if ($(row).children('th').size() != 0 && $(row).children('th')[0].length != 0) {
                $(row).children('th')[0].click();
            }
        }
    }
}


Because the cell click event is only triggered when a scorecard row is not selected, this will usually only happen when the page loads for the first time. This event would also be triggered when changing, for example, the manufacturer filter from “Manufacturer 1” with “Product A” selected in the scorecard to “Manufacturer 2”. As “Manufacturer 2” does not produce “Product A”, the click event is triggered and “Product B” would be the default scorecard selection.

Edit: June 14, 2014

When drilling up on a Scorecard from a root item that is not the first item, the "elem" element will be undefined and therefore a default row member will not get selected. In this case, which is likely to be infrequent, you can just use "table.sctb" as the selector. The revised function is provided below:

function NotifyBrowserOfAsyncUpdate(elem) {
    var elemId, selector, rows, row;

    if (typeof elem === 'undefined') {
        // occurs when drilling up to All in scorecard
        selector = "table.sctb";
    }
    else {
        elemId = $(elem).prop('id');
        selector = '#' + elemId + " table.sctb";
    }

    if ($(selector).length != 0) {
        // scorecard element found! get rows that have class that begin with "r-" (i.e. class|="r")
        rows = $(selector).find('tr[class|="r"]');
        if ($(rows).find('.scs').size() == 0) {
            row = $(rows)[0];
            // find head cell in first row and click
            if ($(row).children('th').size() != 0 && $(row).children('th')[0].length != 0) {
                $(row).children('th')[0].click();
            }
        }
    }
}

Wednesday, May 30, 2012

Breadcrumb Bar Style Dashboard Navigation

I recently needed to provide a breadcrumb on a SharePoint dashboard page which uses web parts to provide information about a sales territory area. Because the sales territories are defined as a hierarchy consisting of thousands of areas (e.g. regions which contain districts, districts which contain trade areas, and trade areas which contain counties), I chose to use a Query String (URL) Filter to pass an area to the dashboard page, rather than trying to load the areas into a PerformancePoint tree filter or multiple cascading filters. As a result, the page is much more light weight and easy to navigate, however, at the expense of having to do full postbacks to navigate between areas.

Conceptually, the area breadcrumb needed to show the area being viewed and the path to that area (i.e. all of its parents). Additionally, the parents could be links, providing a way to navigate to any of the parent areas. A very simplistic approach could display the information in a manner similar to the following:

National > Region 1 > District 1D > Trade Area 1D2 > Orange County, CA

But then I thought of the Breadcrumb Bar that was originally introduced with the Windows Vista version of Windows Explorer. These aren't drives, folders and files, but the hierarchical nature of the sales territories fit perfectly. The benefit of this approach would be to allow additional navigation possibilities to any of an area's parent's children (read on if that's not clear).

With a bit of Googling, I found a nice basis for my breadcrumb bar with this example: CSS-Only Dropdown Menu. It basically provides a very simple, yet elegant, drop down menu with a little javascript to handle the mouseover and mouseout events.

Breadcrumb Bar Example for Orange County, CA (this example should be interactive... at least it was when I originally posted it):




After dummying up an example of how I wanted the breadcrumb bar to look, I then had to decide how I would need to format the data to populate such a control. I came up with the following which required a stored procedure that basically has to determine each of the selected area's parents and the children of each of those parents (you'll have to design your own query based on your database design). Again, the following is what would be needed when passing "Orange County, CA":



AreaName
AreaType
AreaLevel
IsSelected
National
National
1
1
Region 1
Regions
2
0
Region 2
Regions
2
1
District 1A
Districts
3
0
District 1B
Districts
3
0
District 1C
Districts
3
0
District 1D
Districts
3
1
District 1E
Districts
3
0
District 1F
Districts
3
0
Trade Area 1D1
Trade Areas
4
0
Trade Area 1D2
Trade Areas
4
1
Trade Area 1D3
Trade Areas
4
0
Trade Area 1D4
Trade Areas
4
0
Los Angeles County, CA
Counties
5
0
Orange County, CA
Counties
5
1
Santa Barbara County, CA
Counties
5
0
Ventura County, CA
Counties
5
0


As an example, if I pass "District 1D" to the stored procedure, it would return "Region 2" and "National" (i.e. the parents of "District 1D") and "Region 1" (the additional child of "National") and the other children Districts of "Region 2".

Finally, I needed to build the control. I could have easily written a custom web part, but opted for a Data Form Web part instead. This basically required a bit of XSL to build the HTML. Again, I'll leave that exercise to you, the reader.

The final control also included an "enabled" flag that would allow me display the hierarchy, but disable parent areas that users did not have access to. This required an additional argument to the stored procedure of the user accessing the dashboard.

Although this post is light on code, I hope that it may help in creating your own breadcrumb bar for the navigation of a PerformancePoint dashboard.

Sunday, May 20, 2012

SharePoint 2010, SSRS 2012 and Internet Explorer 9 Issues

In SharePoint 2010, when using the RSViewerPage.aspx to view SSRS reports in Internet Explorer 9, you may experience the following problems:
  • Increasing the Zoom percentage to more than 100% on the page’s toolbar results in the top of the report not being visible. Decreasing the Zoom percentage below 100% results in extra whitespace at the top of the report.
  • The Actions->Export flyout menu does not appear.
I have additionally experienced a problem with a PerformancePoint Reporting Services web part that is displayed in a modal dialog called using SharePoint’s SP.UI.ModalDialog.showModalDialog method. When I click a button on the page that calls the method and displays the dialog, the report will update itself based on connections to filters on the page. Works great in IE7, IE8, Chrome and FireFox, however in IE9, the update to the report is only triggered in compatibility mode.

Adding your site to IE9’s Compatibility View settings list (Tools->Compatibility View settings) should fix the functionality. After discovering this workaround on my own, I stumbled across the following Microsoft Knowledge Base article which addresses the Export menu issue:
FIX: SSRS 2008 R2 report or SSRS 2012 report has no Export menu in Internet Explorer 9.0 if you use the out-of-box viewer page in the SharePoint Report Viewer Web Part (http://support.microsoft.com/kb/2616481).
I’m hoping the Zoom problem and my modal dialog report update problem would be addressed by the fix mentioned in the KB article as well. I’ll update this post eventually when we apply the CU.

UPDATE 7/16/12: I installed SQL Server 2012 CU2 (11.0.2325), which is supposed to include the fixes from CU1 (mentioned in the kb article above), and it has not, unfortunately, fixed any of the issues mentioned in this post.

UPDATE 11/14/12: I installed SQL Server 2012 SP1 (11.0.3000) with the same results: none of the issues mentioned in this post have been addressed.

Wednesday, April 11, 2012

SharePoint Designer 2010 and PerformancePoint Web Part Connections (Part 3)

Part 1, Part 2, Part 3

In the two previous parts to this post, I provided a function to connect two PerformancePoint Services (PPS) web parts and a function to connect a SharePoint Filter to a PPS web part. These functions work great, but only if you know the arguments to pass. These arguments can be GUIDs, for example, in the case of connecting a PPS web part to a Scorecard's row or setting a PPS web part's Display Condition based on a PPS Filter.

In this post, I'll solve that problem for you by providing a function that can create all of the function calls for you for a web part page that has existing connections on it. The purpose of this function is to record the connections on the page so that when they are lost (e.g. by saving a web part page using SharePoint Designer), the script can recreate them in seconds.

The Create-Connection-Script function below loops through all of the connections on a web part page, identifying the type connections and creates a script full of function calls to the Create-BIDataProvider-To-TransformableBIDataProvider-Connection and Create-IFilterValues-To-ITransformableFilterValues-Connection functions. If you create the connection script prior to editing and saving a web part page in SharePoint Designer, you'll be able to run the newly created script afterwards and your PPS web parts will be reconnected.

An example of how to call the Create-Connection-Script function for a web part page with exisitng connections:

# Script that contains functions
. M:\ps1\connections\ConnectWebParts.ps1

clear-host

try
{
    $scriptFolder = "M:\ps1\connections"

    if (!(Test-Path -path $scriptFolder))
    {
        New-Item $scriptFolder -type directory
    }

    $date = Get-Date -format "yyMMddHHmmss"

    $web = Get-SPWeb "http://servername/sitecollectionname/webname"
    $wpm = $web.GetLimitedWebPartManager($web.Url + "DashboardPages/Demo.aspx", [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
    $scriptPath = Join-Path $scriptFolder -childpath "$($date)DemoConnections.ps1"    
    Create-Connection-Script $wpm $scriptPath
}
catch [Exception]
{
    write-host $_.Exception.ToString() -ForegroundColor Red
}
finally
{
    $wpm.Dispose() 
    $web.Dispose()
}   
 
The function in it's entirety (place this in a .PS1 file along with the functions provided in Parts 1 and 2):

function Create-Connection-Script {
    param(
        [Parameter(mandatory=$true)] $wpm,
        [Parameter(mandatory=$true)] [string]$scriptPath
    )

    try
    {
        $m_scriptPath = $scriptPath
        clear-script
        
        $wpConnections = $wpm.SPWebPartConnections
        write-script "# This script was generated by $($MyInvocation.MyCommand)" -Color Green
        write-script "# Script that contains functions"
        write-script ". M:\ps1\connections\ConnectWebParts.ps1"
        write-script "# Script for $($wpConnections.Count) connection(s) on web part page ($($wpm.ServerRelativeUrl))." -Color Green
        write-script ""
        write-script "try"
        write-script "{"
        write-script "    `$web = `Get-SPWeb `"$($web.Url)`""
        write-script "    `$wpm = `$web.GetLimitedWebPartManager(`"$($web.Url.Replace($web.ServerRelativeUrl, $wpm.ServerRelativeUrl))`", [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)"
        write-script ""
        
        write-host $web.Url.Replace($web.ServerRelativeUrl, $wpm.ServerRelativeUrl)
        
        foreach ($wpc in $wpConnections)
        {
            write-script "    # $($wpc.Consumer.Title) To $($wpc.Provider.Title)" -Color Green

            if($wpc.ConsumerConnectionPointID -eq "BIDataProvider" -and $wpc.ProviderConnectionPointID -eq "TransformableBIDataProvider")
            {
                write-script "    `$params = @{"
                write-script "        wpm = `$wpm"
                write-script "        wpConsumerTitle =`"$($wpc.Consumer.Title)`""
                write-script "        wpProviderTitle =`"$($wpc.Provider.Title)`""
                
                foreach($record in $wpc.Transformer.ConfigurationState.ProviderConsumerTransformations.Records)
                {
                    write-script "        consumerParameterName = `"$($record.ConsumerParameterName)`""
                    write-script "        providerParameterDisplayName = `"$($record.ProviderParameterDisplayName)`""
                    write-script "        providerParameterName = `"$($record.ProviderParameterName)`""
                    write-script "        providerFormula = `"$($record.ProviderFormula)`""
                }

                $cvr = $wpc.Transformer.ConfigurationState.ConditionalVisibilityRecord         

                if($cvr.IsDefined) 
                {
                    write-script "        cvrIsDefined = `$True"
                    write-script "        cvrProviderParameterDisplayName = `"$($cvr.ProviderParameterDisplayName)`""
                    write-script "        cvrProviderParameterName = `"$($cvr.ProviderParameterName)`""

                    $cvrVisibilitySelections = ""
                    foreach($vs in $cvr.VisibilitySelections)
                    {
                        if($cvrVisibilitySelections.length -ne 0)
                        {
                            $cvrVisibilitySelections += ","
                        }
                        $cvrVisibilitySelections += "`"$vs`""
                    }
                    write-script "        cvrVisibilitySelections = @($($cvrVisibilitySelections))"
                    write-script "        cvrIsDefaultVisibility = $([System.Convert]::ToInt32($cvr.IsDefaultVisibility))"
                }                
                write-script "        reconnect = `$False"
                write-script "    }"
                write-script "    Create-BIDataProvider-To-TransformableBIDataProvider-Connection @params"
            }
            elseif($wpc.ConsumerConnectionPointID -eq "IFilterValues" -and $wpc.ProviderConnectionPointID -eq "ITransformableFilterValues")
            {
                write-script "    `$params = @{"
                write-script "        wpm = `$wpm"
                write-script "        wpConsumerTitle =`"$($wpc.Consumer.Title)`""
                write-script "        wpProviderTitle =`"$($wpc.Provider.Title)`""
                write-script "        mappedConsumerParameterName =`"$($wpc.Transformer.MappedConsumerParameterName)`""
                write-script "        reconnect = `$False"
                write-script "    }"
                write-script "    Create-IFilterValues-To-ITransformableFilterValues-Connection @params"
            }
            else
            {
                write-host "    # Web parts did not have a supported connection" -ForegroundColor Green
            }
            write-script ""
        }
        
        write-script "}"
        write-script "catch [Exception]"
        write-script "{"
        write-script "    `write-host `$_.Exception.ToString() -ForegroundColor Red"
        write-script "}"
        write-script "finally"
        write-script "{"
        write-script "    `$wpm.Dispose()"
        write-script "    `$web.Dispose()"
        write-script "}"
    }
    catch [Exception]
    {
        write-host $_.Exception.ToString() -ForegroundColor Red
    }    
}

function Write-Script {
    param(
        [Parameter(mandatory=$false)] [string]$text,
        [System.ConsoleColor] $color="Gray"
    )
    
    #$text = "$(Get-Date -format g): $text"
    
    write-host $text -ForegroundColor $color
    
    out-file $m_scriptPath -Append -InputObject $text
}
 
Additionally, you'll note the write-script function at the bottom, which I use to write to a file and to the screen.

Hopefully, you can use these functions the way that I have been using them for the past several months, or you can use them to create your own functions to make custom dashboard pages easier to maintain.

Thursday, April 5, 2012

SharePoint Designer 2010 and PerformancePoint Web Part Connections (Part 2)

Part 1, Part 2, Part 3

In Part 1 of this post, I provided a function that can reconnect PerformancePoint Services (PPS) web parts as an alternative to having to reconnect the web parts via the browser. This can come in handy when you've lost all of your PPS connections after having saved your web part page in SharePoint Designer.

In this post, I'm providing a function, Create-IFilterValues-To-ITransformableFilterValues-Connection, that can connect a SharePoint Filter to PPS web parts. I had to create this function to connect Query String (URL) Filters (QSUF) to my PPS web parts. Now this wasn't totally necessary, as these connections are not destroyed by saving your page in SharePoint Designer, but I have used it in various scenarios (e.g. deployment, creating new pages from existing) and it is referenced in the function that I'll provide in Part 3.

An example of connecting a QSUF to a PPS SSRS report:

try
{
    $web = Get-SPWeb "http://servername/sitecollectionname/webname"
    $wpm = $web.GetLimitedWebPartManager("http://servername/sitecollectionname/webname/DashboardPages/demo.aspx", [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)

    # Report: Share by Market Scatter Plot To Area Filter
    $params = @{
        wpm = $wpm
        wpConsumerTitle ="Report: Share by Market Scatter Plot"
        wpProviderTitle ="Area Filter"
        mappedConsumerParameterName ="SqlReportViewUniqueParameterIdSI4"
        reconnect = $False
    }
    Create-IFilterValues-To-ITransformableFilterValues-Connection @params
}
catch [Exception]
{
    write-host $_.Exception.ToString() -ForegroundColor Red
}
finally
{
    $wpm.Dispose()
    $web.Dispose()
}
Here is the function in its entirety:

function Create-IFilterValues-To-ITransformableFilterValues-Connection {
    param(
        [Parameter(mandatory=$true)] $wpm,
        [Parameter(mandatory=$true)] [string]$wpConsumerTitle,
        [Parameter(mandatory=$true)] [string]$wpProviderTitle,
        [Parameter(mandatory=$true)] [string]$mappedConsumerParameterName,
        [bool]$reconnect=$false
    )
    
     try
    {
        $foundConnection = $false
        
        $wpConnections = $wpm.SPWebPartConnections

        # find connection
        foreach ($wpc in $wpConnections)
        {
            if ($wpc.Consumer.Title -eq $wpConsumerTitle -and $wpc.Provider.Title -eq $wpProviderTitle)
            {
                if ($reconnect -ne $true) {
                    $foundConnection = $true
                }
                else {
                    $wpm.SPDisconnectWebParts($wpc)
                }
                break
            }
        }

        if($foundConnection -eq $false)
        {
            # get consumer and provider web parts
            $wpConsumer = $wpm.WebParts | Where {$_.Title -eq "$wpConsumerTitle"}
            $wpProvider = $wpm.WebParts | Where {$_.Title -eq "$wpProviderTitle"}
 
            # get consumer and provider connection points
             $consumerConnectionPoint = $wpm.GetConsumerConnectionPoints($wpConsumer)["IFilterValues"]
             $providerConnectionPoint = $wpm.GetProviderConnectionPoints($wpProvider)["ITransformableFilterValues"]

             if($consumerConnectionPoint -ne $null -and $providerConnectionPoint -ne $null)
             {
                 $transformer = New-Object Microsoft.SharePoint.WebPartPages.TransformableFilterValuesToFilterValuesTransformer
                 $transformer.MappedConsumerParameterName = $mappedConsumerParameterName

                 # connect the web parts
                 $wpConnection = $wpm.SPConnectWebParts($wpProvider, $providerConnectionPoint, $wpConsumer, $consumerConnectionPoint, $transformer) 

                 write-host "Successfully connected: $wpConsumerTitle, $wpProviderTitle" -ForegroundColor Green
             }
             else
             {
                 write-host "Web parts did not have supported ITransformableFilterValues-To-IFilterValues connection types: $wpConsumerTitle, $wpProviderTitle" -ForegroundColor Green
             }
        }
        else
        {
             write-host "Connection already exists: $wpConsumerTitle, $wpProviderTitle" -ForegroundColor DarkCyan
        }
    }
    catch [Exception]
    {
        write-host $_.Exception.ToString() -ForegroundColor Green
    }
    finally
    {
    }    
}
In Part 3 of this post, I'll provide a function that uses the functions from Part 1 and this part to create a connection script for you when you provide it a web part page full of connections.