Search code examples
wpfxaml

Bind two different classes into one Datagrid WPF


I currently have 2 lists with different classes in them: List<Player> and List<Monster>.

I want to get these two lists in a single Datagrid as follows:enter image description here

The reason I need them to be in one Datagrid, is that I need to sort on Initiative, and show the order from high to low. The Datagrid also need to work with any number of players/monsters. The classes look as follows:

public partial class Player
    {
        public bool IsInParty { get; set; }
        public string Name { get; set; }
        public int Ac { get; set; }
        public string ArmorType { get; set; }
        public string[] Speed { get; set; }
        public int InitiativeBonus { get; set; }
        public string[] DmgVul { get; set; }
        public string[] DmgRes { get; set; }
        public string[] DmgImm { get; set; }
        public string[] CondImm { get; set; }
        public string[] Senses { get; set; }
        public string[] Languages { get; set; }
        public NameValuePair[] Conditions { get; set; }
        public int Id { get; set; }


    }
public partial class Monster
    {
        public BaseMonster Stats { get; set; }
        public int Id { get; set; }
        public string Name { get; set; }
        public int Hp { get; set; }
        public List<int> Damage { get; set; }
        public bool IsOverhealed => Hp > Stats.MaxHp;
        public bool IsBloody => Hp <= Stats.MaxHp / 2.0;
        public bool IsNearDeath => Hp <= Stats.MaxHp / 4.0;
        public bool IsDead => Hp <= 0;
        public List<NameValuePair> Conditions { get; set; }
    }
public partial class BaseMonster
    {
        public int DefaultId { get; set; }
        public string DefaultName { get; set; }
        public string Type { get; set; }
        public string Alignment { get; set; }
        public int Ac { get; set; }
        public string ArmorType { get; set; }
        public int MaxHp { get; set; }
        public string HitDice { get; set; }
        public string[] Speed { get; set; }
        public int Str { get; set; }
        public int Dex { get; set; }
        public int Con { get; set; }
        public int Int { get; set; }
        public int Wis { get; set; }
        public int Cha { get; set; }
        public string[] SavThrProf { get; set; }
        public string[] SkillProf { get; set; }
        public string[] DmgVul { get; set; }
        public string[] DmgRes { get; set; }
        public string[] DmgImm { get; set; }
        public string[] CondImm { get; set; }
        public string[] Senses { get; set; }
        public string[] Languages { get; set; }
        public string Challenge { get; set; }
        public NameValuePair[] Traits { get; set; }
        public NameValuePair[] Actions { get; set; }
        public NameValuePair[] LegendaryActions { get; set; }
        public string LairActions { get; set; }
        public string RegionalEffects { get; set; }
    }

Because I am relatively new to front-end and xaml, I have a little trouble how to take on this problem. Currently I've managed to get the following with some test data:

enter image description here

<Grid Grid.Row="2" Grid.Column="1">
            <DataGrid x:Name="creatureDatagrid">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
                    <DataGridTextColumn Header="ID" Binding="{Binding Id}"/>
                    <DataGridTextColumn Header="AC" Binding="{Binding Ac}"/>
                    <DataGridTextColumn Header="HP" Binding="{Binding Hp}"/>

                </DataGrid.Columns>
            </DataGrid>
<Grid/>

As you can see, some values work fine, but in this case AC is not working, because AC in found in BaseMonster in the Monster class


Solution

  • DataGrid columns are generated automatically based on the type of ItemsSource items. See this blogpost to get some more insights about this.

    Since a DataGrid can only be bound to a single type / collection you first need to create a common interface which contains all common properties like

    public interface ICreature
    {
        int Id { get; set; }
        //Other common properties
    }
    

    If the items are not changed at runtime (otherwise use ObservableCollections), you can simply bind the DataGrid to the concatenated collections

    public IEnumerable<ICreature> Creatures { get; } = Players.Concat(Monsters);
    

    This will autogenerate all interface defined properties in your DataGrid. All additional properties, you need to define in XAML since they can't be autogenerated:

    <DataGrid ItemsSource="{Binding Creatures}">
        <DataGrid.Columns>
            <DataGridTextColumn Header="CustomProperty" Binding="{Binding Name}"/>
            <!-- More custom properties -->
        </DataGrid.Columns>
    </DataGrid>
    

    Use PriorityBinding for columns accessing different properties:

    <DataGridTextColumn Header="MixedProperty"
        <DataGridTextColumn.Binding>
            <PriorityBinding>
                <Binding Path="PlayersProperty"/>
                <Binding Path="MonstersProperty"/>
            </PriorityBinding>
        <DataGridTextColumn.Binding>
    </DataGridTextColumn>
    

    Note that autogeneration of columns also can be disabled to define all columns by your own.