Search code examples
c#wpfxamldatagridmaterial-design-in-xaml

Populate DataGridComboBoxColumn using integer fields and an enum as it's display name


I receive a query from database containing an array of users and in the user class I have a byte field named Role. I want my DataGridComboBoxColumn to have 2 items. if Role == 0 it's "Member" and if Role == 1 it's "Moderator".

User.cs

public enum UserRole
{
    Member,
    Moderator
}

public class User
{
    [JsonConstructor]
    public User(int userId, string email, string password, string token, string nickname, byte role, uint coins, int power1, int power2, int power3, int power4, DateTime createTime, DateTime? lastLoginTime)
    {
        this.UserId = userId;
        this.Email = email;
        this.Password = password;
        this.Token = token;
        this.Nickname = nickname;
        this.Role = role;
        this.Coins = coins;
        this.Power1 = power1;
        this.Power2 = power2;
        this.Power3 = power3;
        this.Power4 = power4;
        this.CreateTime = createTime;
        this.LastLoginTime = lastLoginTime;
        this.UserRole = (UserRole)role;
    }

    [JsonPropertyName("userId")]
    public int UserId { get; set; }

    [JsonPropertyName("email")]
    public string Email { get; set; }

    [JsonPropertyName("password")]
    public string Password { get; set; }

    [JsonPropertyName("token")]
    public string Token { get; set; }

    [JsonPropertyName("nickname")]
    public string Nickname { get; set; }

    [JsonPropertyName("role")]
    public byte Role { get; set; }

    [JsonPropertyName("coins")]
    public uint Coins { get; set; }

    [JsonPropertyName("power1")]
    public int Power1 { get; set; }

    [JsonPropertyName("power2")]
    public int Power2 { get; set; }

    [JsonPropertyName("power3")]
    public int Power3 { get; set; }

    [JsonPropertyName("power4")]
    public int Power4 { get; set; }

    [JsonPropertyName("createTime")]
    public DateTime CreateTime { get; set; }

    [JsonPropertyName("lastLoginTime")]
    public DateTime? LastLoginTime { get; set; }

    [JsonIgnore]
    public UserRole UserRole { get; set; }
}

MainWindow.xaml

<materialDesign:DataGridComboBoxColumn
  Header="Role"
  Width="100">
    <materialDesign:DataGridComboBoxColumn.EditingElementStyle>
        <Style TargetType="ComboBox" BasedOn="{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type ComboBox}, ResourceId=MaterialDataGridComboBoxColumnEditingStyle}}" >
            <Setter Property="IsEditable" Value="True" />
            <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=DataContext.Role}"/>
            <Setter Property="DisplayMemberPath" Value="UserRole"/>
        </Style>
    </materialDesign:DataGridComboBoxColumn.EditingElementStyle
</materialDesign:DataGridComboBoxColumn>

Solution

  • There are multiple issues in your code and XAML that you should resolve.

    • A data grid column is not part of the visual tree, which means relative source and element name bindings like your binding the DataContext.Role will not work.
    • Your UserRole type is an enum, but you want to display its constant names in a combo box column as items to select from. The ItemsSource property expects an enumerable type which an enum is not.
    • I assume that you bind an array or collection of Users to your data grid. That means the data context of your column is a User instance. In this case your combo box ItemsSource binding does not make sense, since you are trying to bind the data grid data context, which does not contain the current user and even if it did, the Role property is a byte, not an enumerable.
    • You set the DisplayMemberPath to UserRole, but it will not work, because you want to bind your combobox to constants of the UserRole enum, which does not contain a property UserRole.
    • Your User type contains both Role of type byte, as well as UserRole of type UserRole. It is unclear which property of both you want to use and what the other one is used for.
    • Your enum type has the underlying type int, which is the default. If you use values internally as byte anyway, you might consider changing it to byte.
    • You only set an editing style, but not a non-editing style, which will make the value pop-up when you enter edit mode and and hide it when you leave editing mode.
    • You set the DisplayMemberPath, but I think what you really want to do is bind the Role property as selected item.

    In the following I assume that you want to use the Role property of type byte for binding and that you want to display the constant names of the UserRole enum in your combo box column.

    First, you have to create an enumerable of your enum constants. You could provide a collection in code, that you could bind to, but since it does never change and is currently only used in the data grid, you can create it entirely in XAML using an ObjectDataProvider like this:

    <ObjectDataProvider x:Key="UserRoles" MethodName="GetValues" ObjectType="{x:Type system:Enum}">
       <ObjectDataProvider.MethodParameters>
          <x:Type TypeName="local:UserRole"/>
       </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
    

    Next, we need to create a converter that converts a byte to a UserRole and back.

    public class UserRoleToEnumConverter : IValueConverter
    {
       public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
       {
          return (UserRole)System.Convert.ToByte(value);
       }
    
       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
       {
          return (byte)(int)value;
       }
    }
    

    Now let us put it all together in a data grid and bind to the Role property.

    <DataGrid ItemsSource="{Binding Users}" AutoGenerateColumns="False">
       <DataGrid.Resources>
          <local:UserRoleToEnumConverter x:Key="UserRoleToEnumConverter"/>
          <ObjectDataProvider x:Key="UserRoles" MethodName="GetValues" ObjectType="{x:Type system:Enum}">
             <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="local:UserRole"/>
             </ObjectDataProvider.MethodParameters>
          </ObjectDataProvider>
       </DataGrid.Resources>
       <DataGrid.Columns>
          <materialDesign:DataGridComboBoxColumn Header="Role"
                                                 IsEditable="True"
                                                 Width="100">
             <materialDesign:DataGridComboBoxColumn.EditingElementStyle>
                <Style TargetType="ComboBox" BasedOn="{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type ComboBox}, ResourceId=MaterialDataGridComboBoxColumnEditingStyle}}" >
                   <Setter Property="IsEditable" Value="True" />
                   <Setter Property="ItemsSource" Value="{Binding Source={StaticResource UserRoles}}"/>
                   <Setter Property="SelectedItem" Value="{Binding Role, Converter={StaticResource UserRoleToEnumConverter}}"/>
                </Style>
             </materialDesign:DataGridComboBoxColumn.EditingElementStyle>
          </materialDesign:DataGridComboBoxColumn>
       </DataGrid.Columns>
    </DataGrid>
    

    As I already mentioned above, since you only adapt the editing style, you column might not behave like you expect. I recommend you to not edit the style at all, but rather set the bindings on the column:

    <materialDesign:DataGridComboBoxColumn Header="Role"
                                           IsEditable="True"
                                           Width="100"
                                           ItemsSource="{Binding Source={StaticResource UserRoles}}"
                                           SelectedItemBinding="{Binding Role, Converter={StaticResource UserRoleToEnumConverter}}"/>
    

    This approach will work for your requirements, but using enum constants to display UI texts directly is bad. Think about localization and items with more than a single work. You would not display camel cased text. Going forward, maybe you should consider using a more flexible and robust solution.