beaucrawford.net

Give me data or give me death

About the author

Author Name is someone.
E-mail me Send mail

Recent comments

Don't show

Authors

Tags

Don't show

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    © Copyright 2010

    SSRS – Bug with RDLC data binding and explicitly implemented interface properties

    There seems to be a bug in the ReportViewer control.  It deals with interface properties that are explicitly implemented in a concrete class.  Take, for example, the following interface and concrete implementation:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace ReportViewerDataBindProblem
    {
        public interface IPerson
        {
            string FirstName
            {
                get;
            }
    
            string LastName
            {
                get;
            }
        }
    
        public class Person : IPerson
        {
    
            public Person(string firstName, string lastName)
            {
                _firstName = firstName;
                _lastName = lastName;
            }
    
            private string _firstName;
    
            public string FirstName
            {
                get { return _firstName; }
            }
    
            private string _lastName;
    
            string IPerson.LastName
            {
                get { return _lastName; }
            }
        }
    }

    Notice that I implemented the FirstName property implicitly and the LastName property explicitly.  If I use the IPerson Type in an Object Data Source and then bind a RDLC table against it everything seems to be fine.  The properties for IPerson are available for selection as you might expect:

     

    If I inspect the raw RDL, everything appears fine:

    <Fields>
    <Field Name="FirstName">
      <DataField>FirstName</DataField>
      <rd:TypeName>System.String</rd:TypeName>
    </Field>
    <Field Name="LastName">
      <DataField>LastName</DataField>
      <rd:TypeName>System.String</rd:TypeName>
    </Field>
    </Fields>

    I can then add a ReportDataSource (containing a collection of IPerson) to the ReportViewer instance like:

    List<IPerson> people = new List<IPerson>();
    people.Add(new Person("Bob", "Barker"));
    people.Add(new Person("Bill", "Gates"));
    
    this.reportViewer1.ProcessingMode = ProcessingMode.Local;
    this.reportViewer1.LocalReport.ReportEmbeddedResource = "ReportViewerDataBindProblem.Test.rdlc";
    this.reportViewer1.LocalReport.DataSources.Add(new ReportDataSource("ReportViewerDataBindProblem_IPerson", people));
    this.reportViewer1.RefreshReport();

    However, when I render the report, the “Last Name” column does not contain any data:

     

    This is a design flaw IMHO.  I have not looked into this yet using Reflector but I have a guess as to what the problem is.  The ReportViewer control is probably invoking the GetProperties method (from Type) on the concrete instance.  I stress this because if you simply use BindingFlags.Public (the default for the GetProperties method) then you will obviously not see the explicitly implemented property (since it is considered NonPublic).  The solution is to simply reflect on the type that is being databound (IPerson in this case) and not the concrete type.  An instance of the concrete type can still obviously be passed to the databinding context (since it implements the interface).  Isn’t that, after all, the point of interfaces?


    Categories: SSRS
    Posted by Beau on Sunday, September 21, 2008 12:22 AM
    Permalink | Comments (1) | Post RSSRSS comment feed

    SSRS – Enumerate Report Parameters, part II

    I recently posted a snippet showing how you can enumerate the parameters for an SSRS server report.  Here’s another approach that does the same thing by querying the Catalog table in the ReportServer database.  This table holds some miscellaneous metadata for each report.  Included in that table is a XML fragment that holds information about the report’s parameters.

    Public Shared Function ParameterDisplay(ByVal globals As Microsoft.ReportingServices.ReportProcessing.ReportObjectModel.Globals, ByVal parameters As Parameters) As String
    
    Dim doc As New System.Xml.XmlDocument()
    
    Dim xml As String = Nothing
    
    Using connection As New System.Data.SqlClient.SqlConnection("YOUR_CONNECTION_STRING")
        connection.Open()
    
        Using command As New System.Data.SqlClient.SqlCommand()
            command.Connection = connection
            command.CommandText = "SELECT Parameter FROM Catalog WHERE Path=@Path"
            command.CommandType = System.Data.CommandType.Text
            command.Parameters.AddWithValue("Path", globals.ReportFolder & "/" & globals.ReportName)
            xml = TryCast(command.ExecuteScalar(), String)
        End Using
    End Using
    
    If String.IsNullOrEmpty(xml) Then
        Return ""
    End If
    
    doc.LoadXml(xml)
    
    Dim sb As New System.Text.StringBuilder()
    
    For Each node As System.Xml.XmlNode In doc.SelectNodes("/Parameters/Parameter")
    
        Dim parameterName As String = node.SelectSingleNode("Name").InnerText
        Dim parameterPrompt As String = node.SelectSingleNode("Prompt").InnerText
    
        sb.Append(parameterPrompt)
        sb.Append(": ")
    
        Dim parameter As Parameter = TryCast(parameters(parameterName), Parameter)
    
        If parameter.IsMultiValue Then
    
                        Dim values() As Object = TryCast(parameter.Value, Object())
    
                        For i As Integer = 0 To values.Length - 1
    
                        If values(i) Is Nothing Then
                                sb.Append("NULL")
                        Else
                                sb.Append(values(i))
                        End If
    
                        If i <> values.Length - 1 Then
                                sb.Append(", ")
                        End If
    
                Next i
    
                sb.AppendLine()
            Else
                If parameter.Value Is Nothing Then
                    sb.AppendLine("NULL")
                Else
                    sb.AppendLine(parameter.Value)
                End If
            End If
    
    Next node
    
    Return sb.ToString()
    
    End Function

    The key is that we use the Globals class to access the ReportFolder and ReportName.  This allows us to build the full report path, which should correspond to what’s in the Catalog table.

    An important note is that reports only reference the System assembly (along with the stock SSRS processing assemblies) by default.  To use the above code you must first add references to the following two assemblies:

    System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

    System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

    The nice thing about this approach is that you have access to the friendly name for the parameter (the user Prompt).


    Categories: SSRS
    Posted by Beau on Friday, September 19, 2008 6:44 PM
    Permalink | Comments (1) | Post RSSRSS comment feed

    Reporting Services (SSRS) - Enumerate Parameters in RDL Custom Code

    It turns out that, in an SSRS server report, it is not directly possible to enumerate the parameters and their values.  This is commonly needed when displaying parameter summary tables to the user so they can know which parameters they used to generate the report.

    You will notice that I said it’s not directly possible.  I say this because, well, basically anything and everything is possible when it comes to software (you can always build a new layer of indirection…”).  It really boils down to a question of how much time it will take to develop, how ugly it’s going to be, and what sort of maintenance headaches it will cause.  In the end, if it saves a lot of time, then you can usually deal with a little ugliness.

    Here’s a solution to the aforementioned problem.

    Public Shared Function ParameterDisplay(ByVal reportParameters As Parameters) As String
    
        Dim m_nameMapField As System.Reflection.FieldInfo = reportParameters.GetType().GetField("m_nameMap", System.Reflection.BindingFlags.Instance Or System.Reflection.BindingFlags.NonPublic)
        Dim m_nameMap As System.Collections.Hashtable = TryCast(m_nameMapField.GetValue(reportParameters), System.Collections.Hashtable)
        Dim sb As New System.Text.StringBuilder()
    
        For Each parameterName As String In m_nameMap.Keys
    
            Dim parameter As Parameter = TryCast(reportParameters(parameterName), Parameter)
    
            sb.Append(parameterName)
            sb.Append(": ")
    
            If parameter.IsMultiValue Then
                Dim values() As Object = TryCast(parameter.Value, Object())
    
                For i As Integer = 0 To values.Length - 1
    
                    If values(i) Is Nothing Then
                        sb.Append("NULL")
                    Else
                        sb.Append(values(i))
                    End If
    
                    If i <> values.Length - 1 Then
                        sb.Append(", ")
                    End If
    
                Next i
    
                sb.AppendLine()
            Else
                If parameter.Value Is Nothing Then
                    sb.AppendLine("NULL")
                Else
                    sb.AppendLine(parameter.Value)
                End If
            End If
    
    Next parameterNameReturn sb.ToString()
    
    End Function

    You can then use an RDL expression to reference this function. This expression would simply be:

    =Code.ParameterDisplay(Parameters)

    You will notice that we are reflecting on the Parameters Type.  For a server report, this is an implementation of the Microsoft.ReportingServices.ReportProcessing.ReportObjectModel.Parameters abstract class found in the Microsoft.ReportingServices.ProcessingObjectModel.dll assembly.  This assembly should be located in the following directory:

    C:\Program Files\Microsoft SQL Server\MSSQL.4\Reporting Services\ReportServer\bin\

    In that same directory, there is an assembly named Microsoft.ReportingServices.ProcessingCore.dll.  This assembly holds the concrete implementations for the Types in the previously mentioned assembly.  The type that we care about is Microsoft.ReportingServices.ReportProcessing.ReportObjectModel.ParametersImpl.  If we crack this type open using Reflector we can see that it, unfortunately, is not enumerable.  However, it does maintain an internal Hashtable instance that maps parameter names to their corresponding indexes.   In the above code snippet I can reach in to grab this Hashtable instance and then enumerate it.  It’s ugly, yes, but it works and you won’t have anything to maintain.

    This approach, of course, comes with several caveats:

    1) It requires Full Trust

    2) The code will break if future implementations (e.g. SQL Server 2008, which I have not had time to look into) of the Parameters type change.  I hope they do.

    This problem brings up an perplexing design question.  Why does the processing object model not have a 1:1 relationship with the RDL document structure?  This, in my opinion, is a grotesque oversight.  If, for example, you have a 1:many collection of elements then the processing object model should have a corresponding enumerable collection.  Every property and attribute in RDL should have a matching property in the processing object model.  You should be able to interact with everything in the document structure programmatically at runtime without using ugly hacks.


    Categories: C# | SSRS
    Posted by Beau on Wednesday, September 17, 2008 4:27 PM
    Permalink | Comments (25) | Post RSSRSS comment feed

    Enable HTML in ReportViewer LocalReport

    HTML is available as a rendering extension in server reports rendered by the ReportViewer control (Microsoft.Reporting.WebForms.ReportViewer).  By “server report” I mean a report that is rendered via the ServerReport property (which makes a Web service call to an actual Reporting Services installation).  The report is rendered on the server and sent back to you as a byte[] array. 

    The bummer is that, by default, there is no support for HTML in local reports (reports that have the .rdlc extension and are rendered via the LocalReport property).  In fact, you will get a LocalProcessingException if you attempt to use the “HTML4.0” format name.  In Reporting Services, the rendering extensions are completely extensible via a .config file.  Not so in the Web server control (and the Win Forms control).  In fact, the rendering extensions are hard coded in the class and there are no public extensibility points.  I found this extremely annoying, so I started poking around in Reflector and finally drilled to the following code:


       1: public override IEnumerable<LocalRenderingExtensionInfo> ListRenderingExtensions()
       2: {
       3:     if (this.m_renderingExtensions == null)
       4:     {
       5:         List<LocalRenderingExtensionInfo> list = new List<LocalRenderingExtensionInfo>();
       6:         Html40RenderingExtension extension = new Html40RenderingExtension();
       7:         list.Add(new LocalRenderingExtensionInfo("HTML4.0", extension.LocalizedName, false, typeof(Html40RenderingExtension), false));
       8:         Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer renderer = new Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer();
       9:         list.Add(new LocalRenderingExtensionInfo("Excel", renderer.LocalizedName, true, typeof(Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer), true));
      10:         RemoteGdiReport report = new RemoteGdiReport();
      11:         list.Add(new LocalRenderingExtensionInfo("RGDI", report.LocalizedName, false, typeof(RemoteGdiReport), false));
      12:         ImageReport report2 = new ImageReport();
      13:         list.Add(new LocalRenderingExtensionInfo("IMAGE", report2.LocalizedName, false, typeof(ImageReport), true));
      14:         PdfReport report3 = new PdfReport();
      15:         list.Add(new LocalRenderingExtensionInfo("PDF", report3.LocalizedName, true, typeof(PdfReport), true));
      16:         this.m_renderingExtensions = list;
      17:     }
      18:     return this.m_renderingExtensions;
      19: }


    So, as I mentioned, the rendering extensions are hard coded.  Above, on line 6, you can see that an instance of “Html40RenderingExtension” is created and then passed into the list via a wrapper instance of type LocalRenderingExtensionInfo.  If we look at the constructor for this class we can see that it looks like:

       1: internal LocalRenderingExtensionInfo(string name, string localizedName, bool isVisible, Type type, bool isExposedExternally) : this(name, localizedName, isVisible)
       2: {
       3:     this.m_type = type;
       4:     this.m_isExposedExternally = isExposedExternally;
       5: }

    I found the “isExposedExternally” boolean value somewhat interesting.  In fact, I dug around and found the aforementioned LocalProcessingException is thrown if the rendering extension is not set to be exposed via this field.  Hmmmmm…. that’s interesting.  Well, with the help of some Reflection magic, we can reach in and grab the private “m_isExposedExternally” field and simply updates its value to true.

    The code to do that is as follows:

    Note:  This is for the Microsoft.Reporting.WebForms.ReportViewer type.
       1: private static void EnableFormat(ReportViewer viewer, string formatName)
       2: {
       3:     const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
       4:  
       5:     FieldInfo m_previewService = viewer.LocalReport.GetType().GetField
       6:     (
       7:         "m_previewService",
       8:         Flags
       9:     );
      10:  
      11:     MethodInfo ListRenderingExtensions = m_previewService.FieldType.GetMethod
      12:     (
      13:         "ListRenderingExtensions",
      14:         Flags
      15:     );
      16:  
      17:     object previewServiceInstance = m_previewService.GetValue(viewer.LocalReport);
      18:  
      19:     IList extensions = ListRenderingExtensions.Invoke(previewServiceInstance, null) as IList;
      20:  
      21:     PropertyInfo name = extensions[0].GetType().GetProperty("Name", Flags);
      22:  
      23:     foreach (object extension in extensions)
      24:     {
      25:         if (string.Compare(name.GetValue(extension, null).ToString(), formatName, true) == 0)
      26:         {
      27:             FieldInfo m_isVisible = extension.GetType().GetField("m_isVisible", BindingFlags.NonPublic | BindingFlags.Instance);
      28:             FieldInfo m_isExposedExternally = extension.GetType().GetField("m_isExposedExternally", BindingFlags.NonPublic | BindingFlags.Instance);
      29:             m_isVisible.SetValue(extension, true);
      30:             m_isExposedExternally.SetValue(extension, true);
      31:             break;
      32:         }
      33:     }
      34: }

    Once you do this on your instance of ReportViewer you can then call LocalReport.Render and pass in the format name of “HTML4.0”.  That will obviously generate HTML and any images contained in the report can be captured by using normal <DeviceInfo> options that are passed to the Render method.  Given the fact that HTML rendering is not directly supported, there are a few minor annoyances with how images are processed.  I’ll blog on that someday.

    Enjoy!

    Categories: C# | SSRS
    Posted by Beau on Tuesday, July 01, 2008 9:19 PM
    Permalink | Comments (10) | Post RSSRSS comment feed