James_UK_DEV
James_UK_DEV

Reputation: 519

flatten child objects into properties with LINQ

I have a collection of objects with a child collection. I want to flatten it for reporting purposes with LINQ.

I don't know if this is possible.

For example a list of person objects, each with a child list of features.

Pseudocode:

public sealed class Person
{
    public Name { get; set;}
    IEnumerable<Feature> Features
}

public sealed class Feature
{
    public FeatureName { get; set;} 
    public FeatureValue { get; set;}    
}

Data:

John
    Height 183
    Sex    Male

Jane
    Height 160
    Sex    Female
    Additional Test

Required output:

Name  Height  Sex      Additional

John  183     Male

Jane  160     Female   Test

Effectively I want to bind to:

class Person
{
    public Name { get; set;}
    public Height { get; set;}
    public Sex { get; set;}
    public Additional { get; set;}
}

Edit: Using a dynamic type with Activator.CreateInstance as follows to call a constructor that takes the base Person type:

_results = from person in _people select Activator.CreateInstance(_personWithFeaturesType, person);

Creates the following answer:

System.Linq.Enumerable.WhereSelectEnumerableIterator<Person,object>

Expanding the resultsview when debugging it is a list of the new type I've created and stored in the _personWithFeaturesType member variable.

I don't understand what's being returned, it that a list of Person objects keyed with an object? The WPF binding in the 3rd party grid doesn't seem to handle:

System.Linq.Enumerable.WhereSelectEnumerableIterator<Person,object>

but does handle:

IEnumerable<Person>

Upvotes: 1

Views: 1284

Answers (3)

James_UK_DEV
James_UK_DEV

Reputation: 519

Here's an answer or work around using code behind depending on your viewpoint.
I'm trying to avoid code behind generally for MVVM, but there seems little point using lots of horrendous code instead in this case.

The column is dynamically added in the _Loaded event for the 3rd party grid:

DataGrid.Column featureFromList = new DataGrid.Column();
featureFromList.DisplayMemberBindingInfo = new DataGrid.DataGridBindingInfo();
featureFromList.DisplayMemberBindingInfo.Path = new PropertyPath("Features", null);
featureFromList.DisplayMemberBindingInfo.Path.Path = "Features";
featureFromList.DisplayMemberBindingInfo.ReadOnly = true;
featureFromList.DisplayMemberBindingInfo.Converter = new Converters.FlattenedPersonConverter();
featureFromList.DisplayMemberBindingInfo.ConverterParameter = "Height"; //hard coded, but would be read from database/other objects

A converter takes the list of values and gets the relevant value using the ConverterParameter as the key:

[ValueConversion(typeof(object), typeof(object))]
class FlattenedPersonConverter: IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {
        if (value == null || ((IEnumerable<Feature>)value).Count == 0)
        {
            return null;
        }
        else
        {
            return ((Feature)((IEnumerable<Feature>)value)[parameter.ToString()]).FeatureValue;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }

    #endregion
}

This is psuedocode and might not compile.

It still would be useful to "pivot" the rows of features as columns across automatically.

Upvotes: 0

Richard
Richard

Reputation: 30618

You won't be able to do this automatically. You'll need to set up a mapping to do this at the object level once it's been retrieved from the database. You could consider using something like AutoMapper to configure your mappings, as this will at least allow you to test that you have mappings set up for each property.

For example (and renaming your second Person class PersonDto):

Mapper.CreateMap<Person, PersonDto>()
    .ForMember(dest => dest.Height, opt => opt.MapFrom(
        src => src.Features.FirstOrDefault(f => f.FeatureName == "Height").FeatureValue);

The properties with the same names will be mapped automatically. You may need to handle the feature not being present separately - I don't remember if this gets handled by automapper automatically. You can verify that you have all properties mapped on your destination object by calling:

Mapper.AssertConfigurationIsValid();

Then map your results from the database using:

var results = context.People.Include(p => p.Features).ToList();
var report = Mapper.Map<IEnumerable<PersonDto>>(results);

Upvotes: 1

Konrad Kokosa
Konrad Kokosa

Reputation: 16878

I would go into something like:

List<Person> persons = new List<Person>();
persons.Add(new Person()
{
    Name = "John",
    Features = new List<Feature>()
    {
        new Feature() { FeatureName = "Height", FeatureValue = "183" },
        new Feature() { FeatureName = "Sex", FeatureValue = "Male" }
    }
});
persons.Select(p => new MappedPerson()
{
   Name = p.Name,
   Height = p.Features.Where(f => f.FeatureName == "Height").DefaultIfEmpty(Feature.NullFeature).First().FeatureValue,
   Sex = p.Features.Where(f => f.FeatureName == "Sex").DefaultIfEmpty(Feature.NullFeature).First().FeatureValue
});

Upvotes: 1

Related Questions