Search code examples
c#wpfdatagrid

Swap Items in a DataGrid with Comboboxcolumns?


What I have

I have a UserControl set up in the MVC pattern with a DataGrid and a List of Models. My List has more items than my DataGrid can display, which is why I a looking for a way to build in a way to let my Users select the items they want to have visibily by themselves.

What I need

My DataGrid should be able to do all of the following:

  • Display 8 of up to 30 items of my List
  • Easily allow Users to change the item of each row
  • Update the models in my main list when the user changes cell values in my DataGrid (in case I work with a secondary list of only 8 items)

What It should ideally look like

As I am trying to build a custom charactersheet-generator for the Pen&Paper game DSA(the black eye), I'd be happy to make the DataGrid work as identical to their grid as possible.

Here you can see a grid to select weapon-skills. the first column contains a combobox with the names for all weapon-skills. On selecting a skill from this box the row changes to the stats of this skill

DSAPIC

I previously only worked with DevExpress GridControls, so I struggle to get into 'default' DataGrids, even without implementing the functionality.

Additional Information:

  • I load and save my charactersheets from/to xml files
  • The charactersheets are supposed to be printed

Question

How can I get this to work in the way I imagined it/ Do you have other/easier solutions for my problem?


Solution

  • Two points of interest:

    1. If you want updates to be visible over all collections, the easiest way is to ensure that all collection use the same references to populate themselves.
    2. On a related note, showing the same record more than once will require special care. Updates might seem weird, as they involve several rows. LINQ queries might return another instance then intended. In this case, however, we're only interested in showing unique records, with an option to replace them.

    Approach

    Adding a property IsShown to your row's viewmodel to track if the record is currently visible in the datagrid. We can enforce this using a RowStyle.

    Using a CollectionView for the combobox that's based on the datagrid's ItemsSource. If the record is not shown, it should be selectable from the combobox. Adding a filter to the collectionview should do the trick.

    Code Sample

    WeaponViewModel.cs

    public class WeaponViewModel : ViewModelBase
    {
        private readonly string _name;
        private string abbreviation;
        private int damage;
        private bool isShown;
    
        public WeaponViewModel(string name, string abbreviation, int damage, bool isShown)
        {
            _name = name;
            this.Abbreviation = abbreviation;
            this.Damage = damage;
            this.IsShown = isShown;
        }
    
        public string Name => _name;
    
        public string Abbreviation
        {
            get { return abbreviation; }
            set
            {
                abbreviation = value;
                OnPropertyChanged();
            }
        }
    
        public int Damage
        {
            get { return damage; }
            set
            {
                damage = value;
                OnPropertyChanged();
            }
        }
    
        public bool IsShown
        {
            get { return isShown; }
            set
            {
                isShown = value;
                OnPropertyChanged();
            }
        }
    }
    

    MainViewModel.cs

    public class MainViewModel : ViewModelBase
    {
        public MainViewModel()
        {
            Weapons = new ObservableCollection<WeaponViewModel>(GetWeapons());
            DropDownWeapons = (CollectionView)new CollectionViewSource { Source = Weapons }.View;
            DropDownWeapons.Filter = DropDownFilter;
        }
    
        #region DataGrid
    
        public ObservableCollection<WeaponViewModel> Weapons { get; }
    
        private WeaponViewModel currentWeapon;
        public WeaponViewModel CurrentWeapon
        {
            get { return currentWeapon; }
            set
            {
                currentWeapon = value;
                OnPropertyChanged();
            }
        }
    
        #endregion DataGrid
    
        #region ComboBox
    
        public CollectionView DropDownWeapons { get; } 
    
        private WeaponViewModel selectedWeapon;
        public WeaponViewModel SelectedWeapon
        {
            get { return selectedWeapon; }
            set
            {
                if (value != null)
                {
                    selectedWeapon = value;
                    ReplaceCurrentWith(selectedWeapon);
                    OnPropertyChanged();
                }
            }
        }
    
        private void ReplaceCurrentWith(WeaponViewModel requestedWeapon)
        {
            currentWeapon.IsShown = false;
            requestedWeapon.IsShown = true;
            var currentWeaponIndex = Weapons.IndexOf(currentWeapon);
            var requestedWeaponIndex = Weapons.IndexOf(requestedWeapon);
            Weapons.Move(requestedWeaponIndex, currentWeaponIndex);
            DropDownWeapons.Refresh();
        }
    
        private bool DropDownFilter(object item)
        {
            var weapon = (WeaponViewModel)item;
            return weapon.IsShown == false;
        }
    
        #endregion ComboBox
    
        private static IList<WeaponViewModel> GetWeapons()
        {
            var weapons = new List<WeaponViewModel>
            {
                new WeaponViewModel("Assault Rifle", "AR", 30, true),
                new WeaponViewModel("Submachine Gun", "SM", 17, true),
                new WeaponViewModel("Revolver", "RV", 54, true),
                new WeaponViewModel("Shotgun", "AR", 30, true),
                new WeaponViewModel("Sniper", "SN", 63, true),
                new WeaponViewModel("Rocket Launcher", "RL", 300, true),
                new WeaponViewModel("Grenade Launcher", "GL", 200, true),
                new WeaponViewModel("Minigun", "MG", 20, true),
                new WeaponViewModel("Knife", "KN", 10, false),
                new WeaponViewModel("Baseball Bat", "BB", 6, false),
            };
            return weapons;
        }
    }
    

    MainWindow.xaml

    <Window x:Class="WpfApp.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:vm="clr-namespace:WpfApp.ViewModels"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen">
        <Window.DataContext>
            <vm:MainViewModel/>
        </Window.DataContext>
        <DataGrid ItemsSource="{Binding Weapons}" SelectedItem="{Binding CurrentWeapon}"
                  AutoGenerateColumns="False">
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsShown}" Value="False">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.RowStyle>
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="Weapons">
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                                ItemsSource="{Binding DropDownWeapons}"
                                SelectedItem="{Binding SelectedWeapon}"
                                DisplayMemberPath="Name"
                                IsEditable="False" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Name}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True" />
                <DataGridTextColumn Header="AB" Binding="{Binding Abbreviation}" />
                <DataGridTextColumn Header="Damage"  Binding="{Binding Damage}" />
            </DataGrid.Columns>
        </DataGrid>
    </Window>