Monday, December 29, 2014

Dynamically change column width in Reporting Services 2010

I have a requirement to hide/show columns in a Table depending on the type of document being reported and adjust another column's width to compensate. Changing a column's visibility according to a formula is easy, however there is no facility to change a column's width without using code, which is still a pain in the **** in Reporting Services.

Here are the three formats I need to support...

VENDOR requires an Item Number column and a wide Description


STORES requires a Stock Number column and a wide Description

VFS requires an Item Number column and a Stock Number column and a narrow Description.

Hiding a column is easy - you right click the column's tag, click on column visibility, check the 'Show or hide based on an expression', and then enter the Hidden value. I need to hide the Stock Number column for VENDOR documents so...

But how can you change the width of a column without a Dante-like descent into code?

Fortunately Item Number and Stock Number are both the same width (I made them 0.75" each). That means the Description column needs to be 0.75" narrower when the document is VFS.

I created a new column 0.75" wide to the right of the Description column and merged the header and body cells of the Description column with the header and body cells of the new column. With the Description column's tag selected the report designer looks like this...


Then I told Reporting Services to hide the new column when the document is VFS.


Wednesday, December 3, 2014

Changing SelectedIndex on one combo box triggers SelectionChanged event on a different combo box

WPF 4.0

I just spent ten minutes trying to figure out why changing the selected index on one combo box triggers the SelectionChanged event on a different combo box.

The two combo boxes are defined thus...

<ComboBox Name="SearchTransactionTypeCombo" Binding="{StaticResource TransactionTypes}"  SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}"></ComboBox>

<ComboBox Name="EditTransactionTypeCombo" Binding="{StaticResource TransactionTypes}"  SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}" SelectionChanged="TransactionTypeCombo_SelectionChanged" ></ComboBox>

When I change SearchTransactionTypeCombo.SelectedIndex in code the TransactionTypeCombo_SelectionChanged event is raised and the sender object is EditTransactionTypeCombo. How can this be?

Eventually I thought to look at the DropDownList style in case I had declared an EventSetter in there. Here is the style definition...

    <Style TargetType="{x:Type ComboBox}" x:Key="DropDownList" BasedOn="{StaticResource {x:Type ComboBox}}">
        <Setter Property="IsEditable" Value="False"/>
        <Setter Property="IsSynchronizedWithCurrentItem" Value="True"/>
        <Setter Property="HorizontalAlignment" Value="Stretch"/>
    </Style>

The problem is the IsSynchronizedWithCurrentItem property. Both combo boxes are bound to the same resource. When I changed SearchTransactionTypeCombo.SelectedIndex the current row of the collection was changed, which in turn changed the selected index of EditTransactionTypeCombo which raised the TransactionTypeCombo_SelectionChanged event.

As synchronization is not really needed, I removed it from the style. Alternatively I could have bound to different resources, or overridden synchronization in the control definitions.

Monday, November 24, 2014

Automating Reporting Services Deployment

Let's assume you have reporting services installed on your production servers and you want to deploy reports as part of an automated process. You may have hundreds of production servers at various sites so you need to automate the process.

Reporting Services uses named data sources that are evaluated at run time. You can leverage this by creating a consistently named data source on each Reporting Services server that points to the SQL Server instance that holds your data. The same concept applies if your data is spread over instances. This process can be automated too, but as it only happens once per Report Server instance, I did it manually.

If, as is common, your Reporting Services server is also your SQL Server you can just have all the data sources use (local).

What changes with each new release is the datasets, the reports, and the report parts.

Devenv

If you are pushing from an environment that has Visual Studio, you can simply run devenv to deploy the report project. DevEnv can be found in Program Files\Miscrosoft Visual Studio\<version>\Common7\IDE. The following example deploys all the reports in the PurchasingReports solution using the release configuration and creates a log file in Deployment.log.

devenv.exe PurchasingReports.sln  /deploy release /out Deployment.log

You could create a different configuration for each of your Reporting Services servers and change the /deploy option to deploy to different servers. For example you could have Debug, Release, California, Ukraine, and Taiwan configurations.

But often deployment is done by pulling from the target server. This makes starting and stopping services and other scripting tasks easier. It's unlikely you will have Visual Studio installed on a production server so this devenv approach won't work.

RS.exe

RS.exe is Microsoft's weird way of allowing you to write a VB.Net script that is compiled and executed at run time in an environment that has a specific reporting services SOAP library included. The documentation says you can create the script in Visual Studio but you get no Intellisense or any other features so you may as well just write the script in Notepad. The only important thing is that the script file must have the rss extension.

Let's write a script that pulls reports and data sets from a build server, overwriting any existing copies. You can also pull data sources, security, subscriptions and a host of other things. In addition, you could do a lot of other, non-reporting, things if you wanted.

We will need a batch file to run our script. RS.exe can be found in Program Files\Microsoft SQL Server\120\Tools\Binn. I'm going to target SQL Server 2010 (see the -e option). The batch file looks like this...

rs.exe -i "rsDeploy.rss" -s (local) -e Mgmt2010

-i specifies the path to the rss script
-s specifies the name of the target Reporting Service instance
-e specifies the SOAP endpoint to target. The default is Mgmt2005.

There are other options, but they are not relevant to this post.

Now we have to write rsDeploy.rss. We could do it in Visual Studio or we could do it in Notepad. Remember it's just a console app.

We start by declaring Main(), establishing credentials, and making sure our dataset and report folders exist, Note there is an implied object called rs which is an instance of ReportingService2010 (because of the endpoint we chose). We will make heavy use of this object so it's worth looking at the documentation on MSDN. This example copies reports for my purchasing project. Substitute your own name for <build server>. 

Dim reportFolder As String = "Purchasing"
Dim reportPath as string = "/" & reportFolder
Dim reportFilePath As String = "\\<build server>\PurchasingReports"
Dim reportFileName As String = "*.rdl"
Dim datasetFolder As String = "DataSets"
Dim datasetPath as string = "/" & datasetFolder 
Dim datasetFilePath As String = "\\<build server>\PurchasingReports"
Dim datasetFileName As String = "*.rsd"
Dim definition As [Byte]() = Nothing
Dim warnings As Warning() = Nothing

Public Sub Main()

rs.Credentials = System.Net.CredentialCache.DefaultCredentials
' Create the parent folder
Try
rs.CreateFolder(reportFolder, "/", Nothing)
Console.WriteLine("Parent Folder {0} created successfully", reportFolder)
rs.CreateFolder(datasetFolder, "/", Nothing)
Console.WriteLine("Parent Folder {0} created successfully", datasetFolder)
Catch ex as Exception
Console.WriteLine("Creating Parent Folder {0}:{1}", reportFolder, ex.Message)
End Try

End Sub

Next we will loop through our datasets and deploy them one by one. Add the following code to the end of Main.

' Publish the datasets
Dim datasets as string() = System.IO.Directory.GetFiles(datasetFilePath, datasetFileName)
For Each dataset as string in datasets
PublishDataSet(dataset)
Next

The PublishDataSet method is quite simple. A lot of information is returned in the warnings collection and in the CI object.

Public Sub PublishDataSet(dataset as string)

Dim stream As FileStream
Dim datasetName As String
Dim CI as CatalogItem

Try
Console.Write("Publishing dataset {0}:", dataset)
stream = File.OpenRead(dataset)
definition = New [Byte](stream.length - 1){}
stream.Read(definition, 0, CInt(stream.length))
stream.Close()
datasetName = System.IO.Path.GetFileName(dataset).replace(".rsd","")
CI = rs.CreateCatalogItem("DataSet", datasetName, datasetPath, true, definition, Nothing, warnings)

Console.WriteLine("OK")
Catch ex as Exception
Console.WriteLine(ex.Message)
End Try
End Sub

Publish reports is almost identical. You can see how you could extend this to deploy other reporting services features. Add the following to Main...

' Publish the reports
Dim reports as String() = System.IO.Directory.GetFiles(reportFilePath, reportFileName)
For Each report as String in reports
PublishReport(report)
Next

and add this method too. Note that after deploying the report we need to link it to the consistently named data source I mentioned at the top of the article.

Public Sub PublishReport(report as String)
Dim stream As FileStream
Dim reportName As String
Dim CI as CatalogItem
Try
Console.Write("Publishing {0}:", report)
stream = File.OpenRead(report)
definition = New [Byte](stream.length - 1){}
stream.Read(definition, 0, CInt(stream.length))
stream.Close()

reportName = System.IO.Path.GetFileName(report).replace(".rdl","")
CI = rs.CreateCatalogItem("Report", reportName, reportPath, true, definition, Nothing, warnings)
' Set the DataSource
Dim ds(0) as DataSource
Dim s as new DataSource
Dim dsr As New DataSourceReference
dsr.Reference = "/Data Sources/Purchasing"
s.Item = dsr
s.Name="Purchasing"
ds(0) = s
Dim myItem As String = reportPath & "/" & reportName
rs.SetItemDataSources(myItem, ds)
Console.WriteLine("OK")
Catch ex as Exception
Console.WriteLine(ex.Message)
End Try
End Sub

Now all we have to do is run the batch file, fix all the build errors and we're good to go.

Sunday, October 26, 2014

Cannot access Properties.Settings from App.xaml.vb

I don't generally like the Settings class in the .Net framework because it does not work well with ClickOnce deployment. If you alter the settings collection between deployments, the new deployment thinks the users' settings files are corrupt and refuses to run until you destroy them. This pretty much renders settings useless.

However I noticed another problem the other day while messing around. Let's pretend your project has a setting of "Name" defined as a string with user scope. To access this setting in the main window you would use...

String Name;
Name = Properties.Settings.Default.Name;

Now try the same thing in App.xaml.vb and the compiler will complain that Settings is not valid.


The clue is buried in the error message. It's expecting a dictionary. What's happening here is that the compiler is confusing Properties.Settings with Application.Current.Properties(...) and it's expecting a dictionary key (or some dictionary reference) to follow "Properties".

To reference the settings from the App.xaml.vb you need to explicitly prefix "Properties" with the default namespace ie.

String Name;
Name = Lesson_2.Properties.Settings.Default.Name;

The best approach would be to always use the fully qualified reference for both Properties.Settings and Application.Current.Properties ie.

String Name;
Name = Lesson_2.Properties.Settings.Default.Name;
Application.Current.Properties["Name"] = Name;

Of course, Visual Basic uses the My syntax to access settings which avoids this confusion completely.


Tuesday, October 21, 2014

Reporting Services generates invalid pdf files

Let's be honest here - the fault was all mine although it took a long time to realize why.

This isn't really a WPF issue at all, but it is a WCF issue and I'm only using WCF because the client is in WPF. The question is "How do I tell Reporting Services to render a report and return the pdf to me so I can send it back to the client?" I have a WCF service doing this so I can take more control over parameters, security, etc.

There's dozens of ways to do this and I explored most of them. The one that made most sense to me was to create a web request object and grab the response in a web response. Then I save the ResponseStream to a temporary file which gives me the option to dynamically stitch a bunch of reports together using ExpertPDF and return a single pdf file to the client as a byte array.

You have to be very careful to use the correct methods and understand how they work. For example, System.IO.File.ReadToEnd returns a string which can cause pdf files (which can be binary) to be corrupted. Also Stream.Read does not necessarily read all the bytes you asked for. You have to keep calling it until the stream is fully read.

If you only execute a single call to Stream.Read you may not completely populate the byte array - and your pdf file will be corrupted.

Here's the code to execute a report and save it to a temporary file...

    Public Function RunReportingServicesReport() As String

        Dim URL As String
        Dim Request As System.Net.HttpWebRequest = Nothing
        Dim Response As System.Net.HttpWebResponse = Nothing
        Dim ReportName As String = Nothing
        Dim Stream As System.IO.Stream = Nothing
        Dim Bytes As Byte()
        Dim BytesRead As Integer = 0
        Dim BytesToRead As Integer
        Dim Offset As Integer = 0
        Try
            URL = "http://ReportServer/ReportService?/Reports/MyReport&rs:Format=PDF&rs:Command=Render"
            Request = System.Net.WebRequest.Create(URL)
            Request.PreAuthenticate = True
            Request.Credentials = New System.Net.NetworkCredential("User", "Password", "Domain")

            Response = Request.GetResponse()
            ReportName = System.IO.Path.GetTempFileName()
            Stream = Response.GetResponseStream()
            BytesToRead = Response.ContentLength
            Bytes = New Byte(BytesToRead - 1) {}
            Do While BytesToRead > 0
                BytesRead = Stream.Read(Bytes, Offset, BytesToRead)
                Offset += BytesRead
                BytesToRead -= BytesRead
            Loop
            System.IO.File.WriteAllBytes(ReportName, Bytes)

        Catch ex As System.Exception
            Throw New System.Exception("RunReportingServicesReport:" & ex.Message)
        Finally
            If Stream IsNot Nothing Then Stream.Close()
            If Response IsNot Nothing Then Response.Close()
        End Try

        Return ReportName

    End Function

Monday, September 29, 2014

Poor man's compiler error list

Every now and again I will have an error in my WPF application (no, really, it does happen!) and the compiler gets its panties in a bind. I'll get a shit load of errors from reference.vb and the generated files and hit the max error limit without any real errors actually being displayed. There is no way to override the error limit so how is a developer supposed to fix the error if the compiler won't show it?

Well there is a way.

Look at the Output window and scroll until you see an error message something like this.




You can even double click on it to jump to the line of code that contains the error. The line is highlighted with a blue dash to the left and the text cursor is placed at the point the compiler thinks contains the error.



Thursday, September 25, 2014

Automating ClickOnce deployment and WCF hosting

I'm at the point where I need to deploy my new WPF application and it's WCF services as part of an automated build. This involves learning a little about MSBuild, a complex application that Microsoft has somehow failed to document in any meaningful way. You can find it in the framework folder (Windows\Microsoft.Net\Framework\v<frameworkversion>\)

MSBuild is used by Visual Studio to perform build and publish functions but can be run from the command line. It uses the project file (vbproj or csproj) together with some parameters from the command line to build and/or publish specific configurations. For example...

msbuild Purchasing.vbproj /t:Publish /p:Configuration=Release /p:PublishDir="\\<production server>\Purchasing\PurchasingClient\\" /v:normal > Log.txt

This command instructs msbuild to perform a release build (using the PropertyGroup with a configuration parameter = Release) and then publish the application to the specified shared folder on the production server. The output is written to a log file. I could not get it to publish to a URL, but that might just be a matter of configuring publish to URL in visual studio, looking at the project file, and looking for the PublishURL task. Then you could override it with a /p:PublishURL parameter.

Note the double slash at the end of PurchasingClient\\. This gets around an odd bug. It seems as though a single slash following by a quote is being interpreted as an attempt to embed the quote into the parameter value. A single slash throws an invalid character error. A double slash does what we want.

The big problem is that MSBuild cannot increment the publish version which upsets the clients when they try to download the application in online-only mode. The client reports an error saying that there is already an application with the same version number installed. Although Visual Studio can auto increment publish version, this is not done via MSBuild so we need to find another way.

All we need to do is increment the content of an element with an XPath of \Project\PropertyGroup\ApplicationRevision. There are several ways we can do this. In the end I found it easier to write a very simple app that takes an XML file name and one or more XPaths. If the content of the elements matching those XPaths is numeric, I increment it and save the file. This is only needed for the WPF applications. I called the application ProjectVersionIncrementer. There's probably a better way to handle the namespace issue, but I was in a hurry.

Module ProjectVendorIncrementer Sub Main(Args() As String) Dim XMLFileName As String Dim XMLFile As New System.Xml.XmlDocument Dim XPath As String Dim sValue As String Dim IsIncremented As Boolean = False Dim mgr As System.Xml.XmlNamespaceManager Try XMLFileName = Args(0) XMLFile.Load(XMLFileName) mgr = New System.Xml.XmlNamespaceManager(XMLFile.NameTable) mgr.AddNamespace("m", XMLFile.DocumentElement.NamespaceURI) Console.WriteLine("Loaded " & XMLFileName) For i As Integer = 1 To Args.Count - 1 Console.WriteLine("Searching for XPath " & Args(i)) XPath = Args(i).Replace("/", "/m:").Replace("/m:/m:", "//m:") For Each XMLNode As System.Xml.XmlNode In XMLFile.DocumentElement.SelectNodes(XPath, mgr) sValue = XMLNode.InnerText If IsNumeric(sValue) Then sValue = (CDec(sValue) + 1).ToString() Console.WriteLine(" Incremented " & XMLNode.Name & " to " & sValue) XMLNode.InnerText = sValue IsIncremented = True End If Next Next If IsIncremented Then XMLFile.Save(XMLFileName) Console.WriteLine("Saved Changes") End If Catch ex As Exception Console.WriteLine("ERROR:" & ex.Message) Finally Console.WriteLine("Done") End Try End Sub End Module

So to increment the version numbers of all configurations of a local file called Purchasing.vbproj you would add the following line at the beginning of your publish bat file.

ProjectVersionIncrementer.exe Purchasing.vbproj //Project/PropertyGroup/ApplicationRevision

Follow this with the msbuild line from the top of the blog post and you've got a bat file that will increment the publish number and publish to the server of your choice.

Automating the publication of WCF services is similar, but different. You don't have to worry about versioning because you're not using ClickOnce. On the other hand msbuild won't handle the deployment. There's a utility called MSDeploy that's supposed to do that but it kept complaining about ACLs so, as it's basically just copying files to a known location I used xcopy instead.

The bat file looks like this and is run from the vbproj folder,,,

C:\Windows\Microsoft.Net\Framework\v4.0.30319\msbuild.exe WCFDatabase.vbproj /t:Package /p:Configuration=Release /v:normal > Log.txt

xcopy obj\Release\Package\PackageTmp \\<Production Server>\Purchasing\WCFDatabase /i /s /y

Although our development boxes are all running Windows 7, our test and production servers are still on Windows Server 2003 SP 2 which only runs IIS 6.0 and framework 4.0. Configuring IIS 6.0 to host WCF services requires some manual labor.


  1. Register the framework by browsing to the framework version and running aspnet_regiis.exe /i /enable
  2. Find ServiceModelReg.exe and run it with /r /x /v /y
  3. In IIS management studio create a new virtual directory that points to the directory you copied the WCF service svc file to. Make sure the virtual directory is configured for the correct version of the framework.
  4. Restart IIS using iisreset.exe
I created a single shared directory under Inetpub/wwwroot/Purchasing and then a new subfolder to host each WCF service. The xcopy for the service copies the files via the shared directory to the subfolder. I also created a new web application for each service that points to the service's subfolder. The web applications are configured for read/write/execute. Each one must be in a classic app pool.

If, after doing this, you still get 405 Not Allowed errors it might mean your .svc extensions didn't get registered, Don't panic - it's not difficult but it is tedious.

Right-click on each of the WCF service virtual directories and select Properties. Then click on the [Configuration...] button. If your Application Extensions list does not include .svc you will need to add it. Click [Add...]

Type "c:\windows\microsoft.net\framework\v4.0.30319\aspnet_isapi.dll" into the Executable: textbox and ".svc" into the Extension: textbox. Leave the defaults in place. Be sure to use the correct version of the framework in the executable path. Repeat for each WCF service.