Monthly Archives: April 2014

Practical usage of C# covariance

Variance is a .NET 4.0 concept that allows implicit conversion between instances of generic types. In other words, it permits the corresponding C# compiler (same applies to Visual Basic) to perform implicit type conversions between instances of generic types whose parameters are in the same inheritance chain.

Consider two type definitions:

public class Vehicle {}
public class Bicycle: Vehicle{}

Using generic interfaces for illustration, a generic interface is covariant in T, if an instance instantiated with type T can be replaced with another instance instantiated with type T1 where T1 derives from T, without need of an explicit cast. IEnumerable<T> is covariant, means that an instance of IEnumerable<Vehicle> can be substituted with an instance of IEnumerable<Bicycles> without requiring an explicit cast.

In code:

IEnumerable<Vehicle> vehicles = new List<Vehicle>();
IEnumerable<Bicycle> bikes = new List<Bicycle>();

// covariance allows us to do
vehicles = bikes; 

This is one of those tricky concepts to wrap one’s head around, so in this blog post I give a practical usage of the covariance.

Consider an abstract class, called Parser created for the purpose of parsing files of a given extension. This abstract class will contain an abstract method that should be overridden with actual parsing logic. The class is given as follows:

public abstract class Parser
{
    // other code omitted for clarity
    public abstract ParseResult<IEntity, IParseContext> Parse(string resourcePath);
}

where the return type definition is defined as:

  public class ParseResult<T, TContext>
    {
        public ParseResult(IEnumerable<T> data, IList<ParseError> errors, TContext context) 
        {
            Context = context;
            Errors = errors;
            Data = data;
        }

        public IEnumerable<T> Data { get; private set; }
        public IList<ParseError> Errors { get; private set; }
        public TContext Context { get; private set; }       
    }

Next, consider a concrete implementation as follows:

public class PrnParser : Parser 
{
    private readonly IAipGateway _aipGateway;
    public PrnParser(IAipGateway aipGateway)
    {
       _aipGateway = aipGateway;
    }
 public override ParseResult<IEntity, IParseContext> Parse(string resourcePath)
        {
            var args = new PrnParseArguments(DateTime.UtcNow, _aipGateway, new BufferedTextFileReader(resourcePath));
            return Parse<MeterReading, PrnParseArguments>(args);
        }

       private void ParseResult<MeterReading, PrnParseArguments> Parse(PrnParseArguments args)
       {
           // do the parsing here
       }
}

If you attempt to compile this, the compiler will issue an error indicating that IParseResult<MeterReading, PrnParseArguments> cannot be converted to ParseResult<IEntity, IParseContext> even though the class MeterReading implements interface IEntity and class PrnParseArguments implements interface IParseContext.

In .NET 3.5 and earlier, one way to solve this problem is via explicit casting. So you would do something like this:

public class PrnParser : Parser 
{
    public override ParseResults<IEntity, IParseContext> Parse(string resourcePath)
        {
            var args = new PrnParseArguments(DateTime.UtcNow, _aipGateway, new BufferedTextFileReader(resourcePath));            
            var parsedResult = Parse<MeterReading, PrnParseArguments>(args);
            return  new ParseResults<IEntity, IParseContext>(parsedResult.Data.Cast<IEntity>(), parsedResult.Errors, parsedResult.Context);            
        }
}

However, starting in .NET 4.0 there is an easier way.

What you need to do is define a generic covariant interface and use this interface as the return type of the abstract method. In this context, a covariant interface will allow implementations of the Parse method to return concrete implementations of IParseResult without requiring that explicit cast.

Such an interface could look like this:

   public interface IParseResult<out T, out TContext>
    {
        IEnumerable<T> Data { get; }
        IList<ParseError> Errors { get;  }
        TContext Context { get;  }
        bool Success { get; }
    }

and then we modify ParseResult<T, TContext> to inherit from our new covariant interface IParseResult as follows:

   public class ParseResult<T, TContext> : IParseResult<T, TContext> 
        where T : IEntity 
    {
        // other code emitted for brevity
    }

Note the out parameters indicating that this is a covariant interface. Changing the signature of the abstract method to return our new covariant interface allows the C# compiler to be able to deduce the appropriate is-a relationship between ParseResult<MeterReading, PrnParseArgumets> and IParseResult<IEntity, IParseContext>

It is also easy enough to see the effect of not making this interface covariant by removing either or both out parameters from IParseResult<IEntity, IParseContext>. When you do this, the original compiler error message re-appears. Please note that there are additional criteria that must be satisfied for a generic interface can be made covariant such absence of public property setters on the generic type. For example, if properties IParseResult.Data and IParseResult.Context had public property setters, this interface could not have been made covariant.

In another blog post, I will capture a practical usage of contravariance.

Advertisements

Handling load/save layout on Infragistics XamDataGrid when model changes

We have a data intensive WPF application that his heavily reliant on Infragistics’ XamDataGrid control for tabular representation of data.  Just to get a feel of how reliant we are on this grid, usage in the application is as follows:

  • 20 tabular data views, each hosting an instance of XamDataGrid.
  • Configurable number of map views hosting a map-tip displaying its data using XamDataGrid.
  • 8 configuration editors, using a typical master-details layout, with an instance of XamDataGrid comprising the major control in the master section of the view.
  • 3 notification windows that present errors, alerts or alarms, depending on how you call it, also using a master-details layout.

From a performance and stylistic point of view, XamDataGrid has mostly worked well.   From a usability perspective, despite some of its shortcomings, such as an innate inability to resize a column to display its longest content when selected column header is double-clicked, XamDataGrid, does scores some good points, especially since it allows grouping and field-reordering abilities.

Our users are allowed to re-order the grid layout according to their preference. With this flexibility, however, comes the additional requirement of being able to persist current layout for each and every instance of the grid used in the application as part of the user profile.  Some of our customers, with multiple monitor arrangements, would re-arrange the grid layout in all major tabular data views, saving this as a read-only profile, thereby preventing further modifications by users with restricted privileges.   These requirements, therefore, demand an ability to reliably save and restore the grid layout when its parent container is being unloading and reloaded from the visual tree, respectively, during window closure or complete application shutdown.

One of our customers reported that there was one view that was not persisting its grid layout to profile. This is a problem we were not able to duplicate in house, especially given that our deployment environment does not entirely mimic those of our customers.   Luckily, using privileged access to one of our customer environments, we were able to gain additional insights after experience the problem firsthand.

The Problem:

XamDataGrid exposes an API that provides an ability to save and restore the grid layout. The respective methods are DataPresenterBase.SaveCustomizations and DataPresenterBase.LoadCustomizations. In our application we use the overloads that take and return a string:

LoadCustomizations(string);
public string SaveCustomizations()

These methods mostly work well until your underlying model changes. To demonstrate this problem, we will use a concrete example.

Assume in v1 your application that displays a list of all classic cars in your geographic region of interest, you have defined Car model with the following properties:

 public class Car
    {
        public string Make { get; set; }
        public string Model { get; set; }
        public double RetailPrice { get; set; }
        public string Manufacturer { get; set; }
        public string Color { get; set; }
        public DateTime DateOfManufacture { get; set; }
    }

Assume that you ran v1 of your application and saved a grid layout that consisted of all of the properties in the Car model. In v1.1, you realized that properties Color and DateOfManufacture are no longer applicable to the model, for whatever reason, and removed them. If you attempt to load v1 profile into v1.1 of your application, this does not work. Also, the grid will no longer be able to load and save your customization.

To demo this, create a MainViewModel class that consist of a collection of cars and all commands required to interact with our grid as follows:

public class MainWindowViewModel
    {
        private string _layout;
        private readonly XamDataGrid _dataGrid;
        private string _layoutLocation = "XamDataGrid.Layout.v1.xml";
        public MainWindowViewModel(XamDataGrid dataGrid)
        {
            _dataGrid = dataGrid;
            Cars = new ObservableCollection<Car>();
        }

        public ObservableCollection<Car> Cars { get; set; }

        public ICommand LoadLayoutFromFileCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                    var layout = File.ReadAllText(_layoutLocation);
                    var fields = typeof (Car).GetProperties().Select(p => p.Name).ToArray();
                    var validatedLayout = XamDataGridHelper.ValidDataGridLayoutCustomization(layout, fields);
                    _dataGrid.LoadCustomizations(validatedLayout);
                });
            }
        }

        public ICommand SaveLayoutToFileCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                    var layout = _dataGrid.SaveCustomizations();
                    File.WriteAllText(_layoutLocation, layout);
                });
            }
        }


        public ICommand LoadLayoutFromMemoryCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                    if (!string.IsNullOrEmpty(_layout))
                    {
                        _dataGrid.LoadCustomizations(_layout);
                    }
                });
            }
        }

        public ICommand SaveLayoutToMemoryCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                    _layout = _dataGrid.SaveCustomizations();
                });
            }
        }

        public ICommand LoadDataCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                    Cars.Clear();
                    
                    Cars.Add(new Car {Make = "Toyota", 
                        Model = "Tercel", 
                        RetailPrice = 250,
                        Color = "#FF0000", 
                        DateOfManufacture = new DateTime(1975, 1, 1),
                        Manufacturer = "Toyota"});
                    Cars.Add(new Car { Make = "Honda", 
                        Model = "Accord", 
                        RetailPrice = 500,
                        Color = "#FF0012",
                        DateOfManufacture = new DateTime(1980, 1, 1),
                        Manufacturer = "Honda" });
                    Cars.Add(new Car { Make = "BMW",
                        Model = "M5", 
                        RetailPrice = 12000,
                        Color = "#FF6666",
                        DateOfManufacture = new DateTime(1985, 11, 1),
                        Manufacturer = "Bayerische Motoren Werke" });
      
                    _dataGrid.DataSource = Cars;
                });
            }
        }

        public ICommand ClearGridCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                    _dataGrid.DataSource = null;
                });
            }
        }

        public class RelayCommand : ICommand
        {
            private readonly Predicate<object> _predicate;
            private readonly Action<object> _action;

            public RelayCommand(Predicate<object> predicate, Action<object> action)
            {
                _predicate = predicate;
                _action = action;
            }

            public void Execute(object parameter)
            {
                _action.Invoke(parameter);
            }

            public bool CanExecute(object parameter)
            {
                return _predicate.Invoke(parameter);
            }

            public event EventHandler CanExecuteChanged;
        }
    }

Since we do not want the grid to automatically generate the layouts for us, create a FieldLayoutFactory that creates a field layouts based on properties of our data as follows:

  public class FieldLayoutFactory
    {
        public static FieldLayout CreateFieldLayout(Type type)
        {
            var properties = type.GetProperties(BindingFlags.Public);

            var fl = new FieldLayout();
            foreach (var property in properties)
            {
                fl.Fields.Add(new Field(property.Name, property.PropertyType));
            }

            return fl;
        }
    }

and to complete the code for this demo, this is a simple XAML for the main window.

<Window x:Class="XamDataGridFieldLayoutProblems.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:dataPresenter="http://infragistics.com/DataPresenter"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button Content="Save Layout To File" Command="{Binding SaveLayoutToFileCommand}"/>
            <Button Content="Load Layout from File" Command="{Binding LoadLayoutFromFileCommand}"/>
            <Button Content="Save Layout to Memory" Command="{Binding SaveLayoutToMemoryCommand}"/>
            <Button Content="Load Layout from Memory" Command="{Binding LoadlayoutFromMemoryCommand}"/>
        </StackPanel>
        <StackPanel
            Grid.Row="1"
            Orientation="Horizontal">
            <Button Content="Load Data" Command="{Binding LoadDataCommand}"/>
            <Button Content="Clear Grid" Command="{Binding ClearGridCommand}"/>
        </StackPanel>
        <dataPresenter:XamDataGrid 
            x:Name="MyXamDataGrid"
            Grid.Row="2"
            DataSource="{Binding Cars}"/>
    </Grid>
</Window>

with the code behind:

 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel(MyXamDataGrid);
            MyXamDataGrid.FieldLayouts.Add(FieldLayoutFactory.CreateFieldLayout(typeof(Car)));
        }
    }

With all of these parts in place, compile and run the application, assuming you have access to InfragisticsWPF3.DataPresenter.v12.1.dll and InfragisticsWPF3.v12.1.dll. I did not run this test with 13.1 of the Infragistics toolkit.

When you initially run the application, it does the usual stuff of creating our view model and initializing the grid. After the application has launched, click on button labeled Load Data. We should see data in our grid as follows:

Re-arrange the grid columns as you wish then click on the button to Save Layout to File. My re-arrangement is shown here:

Clicking on Save Layout to File should create an XML file in the same location as your application executable. Depending on how you re-arranged your grid, the content of XamDataGridLayout.v1.xml should look somewhat like this:

<?xml version="1.0" encoding="utf-8"?>
<xamDataPresenter version="12.1.20121.1010" formatVersion="1.6">
  <fieldLayouts>
    <fieldLayout key="Car" fieldList="Make, Model, RetailPrice;Double, Manufacturer, Color, DateOfManufacture;DateTime">
      <fields>
        <field name="Make" extendedInfo="Make" Visibility="Visible" IgnoreFieldVisibilityOverrides="false" row="0" column="0" rowSpan="1" columnSpan="1" isCollapsedInLayout="false" />
        <field name="Model" extendedInfo="Model" Visibility="Visible" IgnoreFieldVisibilityOverrides="false" row="1" column="0" rowSpan="1" columnSpan="1" isCollapsedInLayout="false" />
        <field name="RetailPrice" extendedInfo="RetailPrice;Double" Visibility="Visible" IgnoreFieldVisibilityOverrides="false" row="0" column="1" rowSpan="1" columnSpan="1" isCollapsedInLayout="false" />
        <field name="Manufacturer" extendedInfo="Manufacturer" Visibility="Visible" IgnoreFieldVisibilityOverrides="false" row="1" column="1" rowSpan="1" columnSpan="1" isCollapsedInLayout="false" />
        <field name="Color" extendedInfo="Color" Visibility="Visible" IgnoreFieldVisibilityOverrides="false" row="0" column="2" rowSpan="1" columnSpan="1" isCollapsedInLayout="false" />
        <field name="DateOfManufacture" extendedInfo="DateOfManufacture;DateTime" Visibility="Visible" IgnoreFieldVisibilityOverrides="false" row="1" column="2" rowSpan="1" columnSpan="1" isCollapsedInLayout="false" />
      </fields>
    </fieldLayout>
  </fieldLayouts>
</xamDataPresenter>

Close and re-run your application without any code changes. After the window launches, click on Load Layout from File and observe that the grid layout is successfully restored.

Now, here comes the fun part ……

Comment out properties DateOfManufacture and Color from the Car model, preventing them from being included in our field layout based on the implementation of FieldlayoutFactory. Of course, you have to also comment our those lines that set the Car color during construction for your code to compile.

After making these changes, re-run the code. You should see only columns Make, Model, RetailPrice and Manufacturer columns. Now, click on Load Layout from File again and observe that nothing happens. Previous layout could not be restored. Re-arrange your grid the way you were expecting it to be restored (even though there are missing fields). Click button to save layout to memory. Next, change the ordering of the fields and click to load layout from memory and observe that the grid does not reload what we saved into memory. The grid is now stuck in some state that prevents it from loading or saving its layout.

The Solution
This new and strange behavior is happening because the grid loaded a field layout with columns that do not match the model or field layout definition is was initially provided. If you restore the properties in Car model that were commented out, or validate the layout before submitting to the grid by remove the two outdated fields, the problem goes away.

Add the following helper class to your solution.

        public class XamDataGridHelper
    {
        /// <summary>
        /// This method will remove any field not that is not contained in the list of columns.
        /// Comparison is done using the field name.
        /// </summary>
        /// <param name="fieldNames">list of field names to validate against</param>
        /// <param name="fieldList">comma seperated field list i.e content of fieldList node in layout. <fields></fields></param>
        /// <returns></returns>
        public static string ValidateFieldsLayoutFieldListNode(IList<string> fieldNames, string fieldList)
        {
            if (fieldNames == null)
                throw new ArgumentNullException("fieldNames");

            if (fieldNames.Count == 0)
                throw new ArgumentException("list of field names is empty");

            if (string.IsNullOrEmpty(fieldList))
                return fieldList;


            var fields = fieldList.Split(new[] { ',' });
            var fieldsToKeep = new StringBuilder();

            var splitter = new[] { ';' };
            for (var i = 0; i < fields.Length; i++)
            {
                var field = fields[i];
                var fieldParts = field.Split(splitter);
                if (fieldParts.Length > 0 && fieldNames.Contains(fieldParts[0].Trim()))
                {
                    fieldsToKeep.AppendFormat("{0}{1}", field, ", ");
                }
            }

            return fieldsToKeep.Remove(fieldsToKeep.Length - 2, 2).ToString();
        }

        /// <summary>
        /// This method will remove any field not that is not contained in the list of columns.
        /// Comparison is done using the field name.
        /// </summary>
        /// <param name="fieldNames">list of field names to validate against</param>
        /// <param name="fieldLayout">the field node to validate.  It must begin with <fields></fields></param>
        /// <returns></returns>
        public static XElement ValidateFieldLayoutFieldsNode(IList<string> fieldNames, XElement fieldLayout)
        {
            if (fieldNames == null)
                throw new ArgumentNullException("fieldNames");

            if (fieldNames.Count == 0)
                throw new ArgumentException("list of field names is empty");

            if (fieldLayout == null)
                throw new ArgumentNullException("fieldLayout");

            var fieldsInlayout = fieldLayout.Elements("field");
            var fieldsToKeep = new List<XElement>();
            foreach (var node in fieldsInlayout)
            {
                var name = node.Attribute("name");
                if (name != null && fieldNames.Contains(name.Value))
                {
                    fieldsToKeep.Add(node);
                }
            }

            fieldLayout.RemoveAll();
            fieldLayout.Add(fieldsToKeep);
            return fieldLayout;
        }

        /// <summary>
        /// This method validates a fieldLayout node by calling <code>ValidateFieldLayoutFieldsNode</code>
        /// and ValidateFieldsLayoutFieldListNode.  It attempts to remove any fields that are not part 
        /// of the validFields list.
        /// </summary>
        /// <param name="xml">the layout </param>
        /// <param name="validFields">valid fields to remove</param>
        /// <returns></returns>
        public static string ValidDataGridLayoutCustomization(string xml, IList<string> validFields)
        {
            var xdoc = XElement.Parse(xml);
            var fieldlayout = xdoc.XPathSelectElement("fieldLayouts/fieldLayout");

            var fieldListNode = fieldlayout.Attribute("fieldList");
            if (fieldListNode != null)
            {
                var fieldList = fieldListNode.Value;
                var cleanedFieldList = ValidateFieldsLayoutFieldListNode(validFields, fieldList);
                fieldListNode.SetValue(cleanedFieldList);
            }

            ValidateFieldLayoutFieldsNode(validFields, fieldlayout.Element("fields"));

            return xdoc.ToString();
        }
    }

Then modify the command handler that loads layout from file so the command definition looks like this:

   public ICommand LoadLayoutFromFileCommand
        {
            get
            {
                return new RelayCommand((args) => true, o =>
                {
                   <b> var layout = File.ReadAllText(_layoutLocation);
                    var fields = typeof (Car).GetProperties().Select(p => p.Name).ToArray();
                    var validatedLayout = XamDataGridHelper.ValidDataGridLayoutCustomization(layout, fields);</b>
                    _dataGrid.LoadCustomizations(validatedLayout);
                });
            }
        }

Compile and run the application. Now click on the button to Load Layout from File and observe that the layout is restored even though some of the fields are missing as show here:

These kinds of problems are difficult to encounter in house especially if there is a discrepancy between test and deployment environments. Also application/profile versioning is always a challenge. While we take full responsibility for this bug and fixed it, I think Infragistics could have done a better job here. If they had failed-fast by say, throwing an exception since provided layout does not match current grid layout, this problem would have been spotted earlier. Note that this is not a generalization or criticism of the quality of their coding guide lines but hopefully a means to share common pitfalls to be aware of when dealing with third party APIs. At least Infragistics has promised to fix this in a future release.

Hoping this helps others that run into a similar problem.