Thursday, May 18, 2023

Watch your enums

I just fixed a bug that caught me by surprise. I know enums are just integers internally, but I always assumed that if you try to compare enums of different types, you would get a build error. You don't.

Start a new Visual Studio VB console application and call it WrongEnum. I used .Net Core 6.0. Make Program.vb look like this.

Option Strict On
Module Program
    Public Enum Enum1
        Success
        Fail
    End Enum
 
    Public Enum Enum2
        Fail
        Success
    End Enum
 
    Sub Main(args As String())
 
        Dim e As Enum1 = Enum1.Success
        If e = Enum2.Success Then
            Console.WriteLine("Success!")
        Else
            Console.WriteLine("Fail")
        End If
        Console.Read()
    End Sub
End Module

I would have thought the line starting If e = would cause a build error because Enum1 shouldn't be comparable to Enum2. But they're both integers, so I guess the compiler is OK with it. Still, not even a warning?

Run the program and it displays "Fail", even though e was initialized as Success. 

Update:

You can also pass the wrong type of enum to a method although this causes a build error if you have Option Strict On.

Consider the method below. You can call it with Enum1 or even an integer when Option Strict is Off. ie PrintSuccess(Enum1.Success). It writes "Fail" to the console.

If you call the method with an integer that is in range it writes the enum's name but if you call it with an integer that is out of range it writes the integer.

        PrintSuccess(Enum1.Success)
        PrintSuccess(1)
        PrintSuccess(2)

Outputs
        Fail         Success         2


    Sub PrintSuccess(e As Enum2)
        Console.WriteLine(e.ToString())
    End Sub

Monday, May 15, 2023

Pop quiz on Event Handlers

Just for fun, I wrote this console app in Visual Basic that adds two identical event handlers and declares a method as Handles. Then I kept raising the event while calling RemoveHandler. See if you can guess what the output will be.

Imports System
 
Module Program
    Dim WithEvents c As New DemoClass()
    Sub Main(args As String())
        Console.WriteLine("-------------------------------")
        Console.WriteLine("Add Handlers")
        AddHandler c.SaySomething, AddressOf SaySomething
        AddHandler c.SaySomething, AddressOf SaySomething
        c.TalkToMe("Two handlers")
        Console.WriteLine("Remove Handler")
        RemoveHandler c.SaySomething, AddressOf SaySomething
        c.TalkToMe("One handler")
        Console.WriteLine("Remove Handler")
        RemoveHandler c.SaySomething, AddressOf SaySomething
        c.TalkToMe("No handlers")
        Console.WriteLine("Remove Handles")
        RemoveHandler c.SaySomething, AddressOf SaySomethingElse
        c.TalkToMe("Not even Handles")
        Console.WriteLine("Press Enter to exit")
        Console.ReadLine()
    End Sub
 
    Private Sub SaySomething(Something As String)
        Console.WriteLine($"Say {Something}")
    End Sub
 
    Private Sub SaySomethingElse(Something As String) Handles c.SaySomething
        Console.WriteLine($"Also Say {Something}")
    End Sub
 
    Public Class DemoClass
        Public Event SaySomething(Something As String)
        Public Sub TalkToMe(Something As String)
            RaiseEvent SaySomething(Something)
        End Sub
    End Class
End Module

The result is that RemoveHandler only removes one of a duplicate pair of handlers. I don't know which one and I don't think it matters. Note the handler declared with Handles is raised before the ones declared with AddHandler, which makes sense. Even more interestingly RemoveHandler will also remove the handler that was declared with Handles. I did not know that. Did you?






Thursday, May 11, 2023

AutoFac

I have been pushing for adoption of Dependency Injection at work for a while now and I'm finally getting some traction. Of course, it's not because it's the right thing to do, but because it enables us to do something we want to do. I can live with that.

I wrote a blog entry about MEF a while back (I just checked and it was four years ago!). My boss came across a container called AutoFac which I did not know about so I've been looking at it. I am particularly interested in how the different types of registration affect the timing of the constructors.

Create a new C# .Net WPF project (I used Framework 4.5.1 but it should work with Core too). Call it DemoAutoFac. Use NuGet to add the AutoFac package.


Change App.xaml.cs to look like this.

using System.Windows;
 
namespace DemoAutoFac
{
    public partial class App : Application
    {
        public static Autofac.IContainer Container { get; set; }
    }
}

and change MainWindow.xaml.cs to look like this.

using Autofac;
using System;
using System.Windows;
 
namespace DemoAutoFac
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Console.WriteLine("-------------------------");
            Console.WriteLine("MainWindow");
            CreateAutoFacContainerByType();
            WriteDate();
            WriteDate();
 
            Console.WriteLine("");
            CreateAutoFacContainerByInstance();
            WriteDate();
            WriteDate();
        }
 
        public void CreateAutoFacContainerByType()
        {
            Console.WriteLine("CreateAutoFacContainerByType");
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ConsoleOutput>().As<IOutput>();
            builder.RegisterType<TodayWriter>().As<IDateWriter>();
            DemoAutoFac.App.Container = builder.Build();
            Console.WriteLine("Exit CreateAutoFacContainerByType");
        }
 
        public void CreateAutoFacContainerByInstance()
        {
            Console.WriteLine("CreateAutoFacContainerByInstance");
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterInstance<ConsoleOutput>(new ConsoleOutput()).As<IOutput>();
            builder.RegisterInstance<TodayWriter>(new TodayWriter()).As<IDateWriter>();
            DemoAutoFac.App.Container = builder.Build();
            Console.WriteLine("Exit CreateAutoFacContainerByInstance");
        }
 
        public void WriteDate()
        {
            DemoAutoFac.App.Container.Resolve<IDateWriter>().WriteDate();
        }
    }
 
    public interface IOutput
    {
        void Write(string content);
    }
 
    public class ConsoleOutput : IOutput
    {
        public ConsoleOutput()
        {
            Console.WriteLine("ctor ConsoleOutput");
        }
 
        public void Write(string content)
        {
            Console.WriteLine("ConsoleOutput.Write");
            Console.WriteLine(content);
        }
    }
 
    public interface IDateWriter
    {
        void WriteDate();
    }
 
    public class TodayWriter : IDateWriter
    {
        public TodayWriter()
        {
            Console.WriteLine("ctor TodayWriter");
        }
 
        public void WriteDate()
        {
            IOutput output = DemoAutoFac.App.Container.Resolve<IOutput>();
            output.Write(DateTime.Today.ToShortDateString());
        }
    }
}
 
The Autofac container is hosted by App so it is reachable from anywhere. Alternatively it could be injected via a property in each class. That's a bit more work. If you run the application the Immediate Window output looks like this...



You can see when we use RegisterType, the constructors are not called until the class is used. However, the constructor is called EACH TIME we call WriteDate. We could use a Singleton pattern, but we would have to use that for each registered class and it will not work with a strict singleton pattern. You'd have to have a non-singleton wrapper around a singleton object because AutoFac wants to call a constructor.

The output after the blank line shows the results of RegisterInstance. The constructors are called as I register the instances, but they are only ever called once. The TodayWriter.WriteDate method can access the container to fetch a reference to the correct IOutput class. 

I prefer the second method. You can avoid delays while registering instances by implementing just-in-time initialization. 

Monday, May 1, 2023

Another Infragistics XamDataGrid bug

I came across yet another Infragistics XamDataGrid bug the other day. I sent it to Infragistcs support, which is normally very good, but the guy working on this is having trouble understanding why this is a problem.

The bug concerns a parent layout that has two child layouts. It displays expanders and labels when it should not. Here's a sample XAML and code.

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp9"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <igDP:XamDataGrid DataSource="{Binding Parents}">
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False" ExpansionIndicatorDisplayMode="CheckOnDisplay"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Key="Parents">
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Parent Name" Name="ParentName"/>
                        <igDP:Field Name="Children1"/>
                        <igDP:Field Name="Children2"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
                <igDP:FieldLayout ParentFieldLayoutKey="Parents" ParentFieldName="Children1" Key="Children1">
                    <igDP:FieldLayout.Settings>
                        <igDP:FieldLayoutSettings LabelLocation="Hidden"/>
                    </igDP:FieldLayout.Settings>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Name" Name="ChildName"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
                <igDP:FieldLayout ParentFieldLayoutKey="Parents" ParentFieldName="Children2" Key="Children2">
                    <igDP:FieldLayout.Settings>
                        <igDP:FieldLayoutSettings LabelLocation="Hidden"/>
                    </igDP:FieldLayout.Settings>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Name" Name="ChildName"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
 
    </Grid>
</Window
>
 
----------------------------------------------

Class MainWindow
 
    Public Class cParent
        Public Property ParentName As String
        Public Property Children1 As List(Of cChild)
        Public Property Children2 As List(Of cChild)
    End Class
 
    Public Class cChild
        Public Property ChildName As String
    End Class
 
    Public Property Parents As New List(Of cParent) From
        {
            New cParent() With {.ParentName = "Parent1",
                                .Children1 = New List(Of cChild) From {New cChild() With {.ChildName = "Parent1.Child1"}},
                                .Children2 = New List(Of cChild) From {New cChild() With {.ChildName = "Parent1.Child2"}}},
            New cParent() With {.ParentName = "Parent2",
                                .Children1 = Nothing,
                                .Children2 = Nothing}
    }
End
Class
 
As you can see, I have hidden the labels on the child rows and expansion indicators are visible only on parents that have children. Parent1 has children and Parent2 does not.

The result looks like this.


I am not expecting Parent2 to have an expander button and I am not expecting the "Children1" and "Childrent2" labels because their location is set to hidden. I'm expecting it to look a bit like this when parent1 is expanded.

If I remove either of the child bands the grid behaves the way I expect.

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp9"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <igDP:XamDataGrid DataSource="{Binding Parents}">
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False" ExpansionIndicatorDisplayMode="CheckOnDisplay"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Key="Parents">
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Parent Name" Name="ParentName"/>
                        <igDP:Field Name="Children1"/>
                        <!--<igDP:Field Name="Children2"/>-->
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
                <igDP:FieldLayout ParentFieldLayoutKey="Parents" ParentFieldName="Children1" Key="Children1">
                    <igDP:FieldLayout.Settings>
                        <igDP:FieldLayoutSettings LabelLocation="Hidden"/>
                    </igDP:FieldLayout.Settings>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Name" Name="ChildName"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
                <!--<igDP:FieldLayout ParentFieldLayoutKey="Parents" ParentFieldName="Children2" Key="Children2">
                    <igDP:FieldLayout.Settings>
                        <igDP:FieldLayoutSettings LabelLocation="Hidden"/>
                    </igDP:FieldLayout.Settings>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Name" Name="ChildName"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>-->
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
 
    </Grid>
</Window
>
 


Now Parent2 does not have an expander and the labels are not displayed.

Update: So Infragistics explained that the blue tags are not labels but expanders. When you have a single child band you don't see them and when you expand the parent you immediately see just the children as in the image above. When there are multiple child bands you see the expanders first and you use them to decide which child band to expand.

OK, this makes sense. Except that there's no way to override this behavior. Also, I would expect the XamDataGrid to realize that a collection of empty collections contains no data, so Parent2 doesn't need an expander.

Here's the response from Infragistics support..

Unfortunately  there is no way you can remove expandable field records( expander labels) with XamDatagrid and that is why we build another control called XamTreeGrid.

Seriously? You built another control just because you couldn't fix this? I could have saved you a lot of time. You have to collapse the expander record in the RecordInitialized event then take responsibility for expanding and collapsing its children yourself.

Using the project above, add some event handlers to the XamDataGrid and then write them.

        <igDP:XamDataGrid DataSource="{Binding Parents}"
                          InitializeRecord="XamDataGrid_InitializeRecord"
                          RecordExpanded="XamDataGrid_RecordExpanded"
                          RecordCollapsed="XamDataGrid_RecordCollapsed">

----------------------------------------------------

    Private Sub XamDataGrid_InitializeRecord(sender As Object, e As Events.InitializeRecordEventArgs)
 
        Dim er As ExpandableFieldRecord = TryCast(e.Record, ExpandableFieldRecord)
        Dim dr As DataRecord
        Dim dataItem As cParent
        Dim diType As Type
        Dim PI As PropertyInfo
        Dim collection As IList
 
        If er IsNot Nothing Then
            er.ExpansionIndicatorVisibility = Visibility.Collapsed
            dr = DirectCast(er.ParentRecord, DataRecord)
            dataItem = TryCast(dr.DataItem, cParent)
            diType = dataItem.GetType()
            PI = diType.GetProperty(er.Field.Name)
            collection = TryCast(PI.GetValue(dataItem), IList)
            If collection Is Nothing OrElse collection.Count = 0 Then
                er.Visibility = Visibility.Collapsed
            End If
        End If
    End Sub
 
    Private Sub XamDataGrid_RecordExpanded(sender As Object, e As Events.RecordExpandedEventArgs)
        SyncExpandableRecords(TryCast(e.Record, DataRecord))
    End Sub
 
    Private Sub XamDataGrid_RecordCollapsed(sender As Object, e As Events.RecordCollapsedEventArgs)
        SyncExpandableRecords(TryCast(e.Record, DataRecord))
    End Sub
 
    Private Sub SyncExpandableRecords(dr As DataRecord)
        dr?.ChildRecords.OfType(Of ExpandableFieldRecord).ToList().ForEach(Sub(er) er.IsExpanded = dr.IsExpanded)
    End Sub

The XamDataGrid_InitializeRecord event handler collapses the expansion indicator for all Expandable records. If the IList it is expanding is empty the entire expandable record is collapsed. Note cParent could be replaced with Object to be more generic.

When we collapse the expander record, it will no longer be able to expand/collapse its children so we have to take responsibility for that ourselves when the parent record is expanded or collapsed.

I have not tested this with more complex hierarchies or when binding to datasets. But clearly, it can be done.

Ta Da!