Reputation: 519
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
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
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
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