How do I handle null nested objects in RDLC report that is bound to custom assembly object datasource?

I have an RDLC report that I am rendering directly to the Response Stream as PDF (rather than using the ReportViewer). In the code that renders the report, it’s DataSource is bound to a List(Of ClassA) objects defined in a custom assembly. This seems to work for the most part. My problem is that I can’t seem to handle the situation where a nested object is null. For example, given ClassA and ClassB (the nested object) defined as follows:

    Public Class ClassA
       Public Id As Integer
       Public Name As String
       Public TheNestedObject As ClassB
    End Class

    Public Class ClassB
       Public Id As Integer
       Public Name As String
       Public TheParentObject As ClassA
    End Class

Whenever I try to conditionally display an “N/A” if Class B is null in my expression as follows:

=IIf(IsNothing(Fields!TheNestedObject.Value,"n/a", Fields!TheNestedObject.Value.Name))

the report displays “#Error” if TheNestedObject is null. If TheNestedObject is not null, it correctly displays the Name.

What am I doing wrong here?

Thanks!!!

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

The iif function evaluates all arguments and thus Fields!TheNestedObject.Value.Name gets evaluated and gives the error since Fields!TheNestedObject.Value is null.

I ended up adding some custom code to the report. It’s under report properties -> Code tab.

Public Function GetName(ByRef obj As Object) As String
    If obj Is Nothing Then
        Return "n/a"
    Else : Return obj.Name
    End If
End Function

And then your textbox expression is:

=Code.GetName(Fields!TheNestedObject.Value)

The function returns “n/a” when it’s null and the Name property when it’s not.

Method 2

I don’t know for sure how to fix your problem, but I have a couple of suggestions…

  1. Perhaps if you changed your IIF statement to be:
    IIf(IsNothing(Fields!TheNestedObject,"n/a", Fields!TheNestedObject.Value.Name))

    IIf always evaluates all of the arguments, so it is trying to evaluate TheNestedObject.Value. If TheNestedObject is NULL or NOTHING, then I wouldn’t be surprised to see it throw an error.

  2. Another idea would be to modify your constructor to add an “empty” “B” object whenever there is no “B.” For example, A.TheNestedObject would point to a “B” object which has no data. B.Id would be 0 (by default) unless you made it a nullable Int. B.Name would be “”. Etc.

Method 3

=iif(First(Fields!model.Value, "model") Is Nothing, "value is NULL", "value is not NULL")

Method 4

=IIf(IsNothing(Fields!TheNestedObject.Value,"n/a", Fields!TheNestedObject.Value.Name))

While your expression would need to look like the following, to be syntactically correct, it does still evaluate the false part before using its result as a parameter for the IIf() function and therefore will still show an #Error:

=IIf(IsNothing(Fields!TheNestedObject.Value),"n/a", Fields!TheNestedObject.Value.Name)

I first deal with this shortcomings of SSRS/RDLC by implementing the Null object pattern.
Implementing this manually is of course too much effort when more then one or two domain objects are involved.

However, since I am already using AutoMapper, @LucianBargaoanu correctly pointed out in the comments, that null objects are supported as an opt-in feature by AutoMapper, so there is no explicit implementation needed.
I therefore use a combination of AutoMapper with its AllowNullDestinationValues, AllowNullCollections, PreserveReferences(), NullSubstitute and ForAllPropertyMaps() feature, to map all my domain classes to report specific classes and substitute all null references to either null objects (when mapping domain object null references to report objects) or reasonable default values (e.g. an empty string for null strings or the default value of the underlying primitive type for Nullable<PrimitiveType>).

Here is some sample code to demonstrate the approach:

namespace Domain
{
    public class MyClass
    {
        public int Id {get; set;}
        public string Name {get; set;} // could be null
        public string Code {get; set;} // could be null
        public decimal? SomeNullableValue {get; set;} // could be null

        public MyOtherClass OptionalOtherClass {get; set;} // could be null
    }

    public class MyOtherClass
    {
        public int OtherId {get; set;}
        public string OtherName {get; set;} // could be null
        public decimal? SomeOtherNullableValue {get; set;} // could be null
    }
}

namespace ReportViewModels
{
    [Serializable]
    public class MyClass
    {
        public int Id {get; set;}
        public string Name {get; set;} // should not be null (but empty)
        public string Code {get; set;} // should not be null (but empty)
        public decimal? SomeNullableValue {get; set;} // should not be null (but default(decimal))

        public string CommonName
            => (Name + " " + Code).Trim();

        public MyOtherClass OptionalOtherClass {get; set;} // should not be null (but a MyOtherClass null object)
    }

    [Serializable]
    public class MyOtherClass
    {
        public int OtherId {get; set;}
        public string OtherName {get; set;} // should not be null (but empty)
        public decimal? SomeOtherNullableValue {get; set;} // should not be null (but default(decimal))
    }
}

public partial class Form1 : Form
{
    private Context _context;
    private ReportObjectGenerator _reportObjectGenerator;
    
    public Form1(Context context, ReportObjectGenerator reportObjectGenerator)
    {
        _context = context;
        _reportObjectGenerator = reportObjectGenerator;

        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        var myDomainObjects = context.MyClass
            .Include(e => e.OptionalOtherClass)
            .ToList();

        var myReportViewModels = _reportObjectGenerator.GetReportObjects<Domain.MyClass, ReportViewModels.MyClass>(myDomainObjects);

        components ??= new System.ComponentModel.Container();
        
        //reportViewer1.LocalReport.ReportEmbeddedResource = "MyNamespace.Report1.rdlc";
        reportViewer1.LocalReport.ReportPath = "Report1.rdlc";
        reportViewer1.LocalReport.DataSources.Clear();
        reportViewer1.LocalReport.DataSources.Add(
            new ReportDataSource
            {
                Name = "MyClassDataSet",
                Value = new BindingSource(components)
                {
                    DataMember = "MyClass",
                    DataSource = myReportViewModels
                }
            });
        
        reportViewer1.RefreshReport();
    }
}

public class ReportObjectGenerator
{
    public List<TDestination> GetReportObjects<TSource, TDestination>(
        IEnumerable<TSource> sourceObjects)
    {
        var domainNamespace = typeof(TSource).Namespace ?? throw new InvalidOperationException();
        var reportNamespace = typeof(TDestination).Namespace ?? throw new InvalidOperationException();

        var mapper = new MapperConfiguration(
                cfg =>
                {
                    cfg.AllowNullDestinationValues = false;
                    cfg.AllowNullCollections = false;

                    var allTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).ToList();
                    var allDomainTypes = allTypes.Where(t => t.Namespace?.StartsWith(domainNamespace) ?? false).ToList();
                    var allReportTypes = allTypes.Where(t => t.Namespace?.StartsWith(reportNamespace) ?? false).ToList();

                    foreach (var reportClassType in allReportTypes)
                    {
                        var domainClassType = allDomainTypes.Single(t => t.Name == reportClassType.Name);

                        cfg.CreateMap(domainClassType, reportClassType)
                            .PreserveReferences();
                    }

                    // If we want to set the default value of the underlying type of Nullable<UnderlyingType>
                    // properties in case they would be null, than AllowNullDestinationValues is not enough and we
                    // need to manually replace the null value here.
                    cfg.ForAllPropertyMaps(
                        pm => pm.SourceMember.GetMemberType().IsNullableType(),
                        (p, _) => p.NullSubstitute ??= Activator.CreateInstance(p.SourceMember.GetMemberType().GetTypeOfNullable()));
                })
            .CreateMapper();

        return mapper.Map<IEnumerable<TSource>, List<TDestination>>(sourceObjects);
    }
}


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x