29

I've looked at the answers to various questions, but haven't managed to map the content in the answers to the problem I'm attempting to solve. I've reduced it down to the following code (representative of the outcome I'm trying to achieve), and basically want to be able to render the Person.TitleId as its corresponding Title.TitleText when the row isn't being edited, and have the drop-down bound correctly so that it displays the TitleTexts in the drop-down and writes the associated TitleId back to the Person record when its changed.

In short, what do I put in my <DataGridComboBoxColumn> to achieve this?

App.xaml.cs

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var viewModel = new ViewModels.MainWindowViewModel();
    var mainWindow = new MainWindow();
    mainWindow.DataContext = viewModel;
    mainWindow.ShowDialog();
}

MainWindow.xaml

<Grid>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Path=Contacts}">
        <DataGrid.Columns>
            <DataGridComboBoxColumn Header="Title" SelectedItemBinding="{Binding Person}">
                <DataGridComboBoxColumn.ElementStyle>
                    <Style TargetType="ComboBox">
                        <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Titles}"/>
                        <Setter Property="IsReadOnly" Value="True"/>
                    </Style>
                </DataGridComboBoxColumn.ElementStyle>
                <DataGridComboBoxColumn.EditingElementStyle>
                    <Style TargetType="ComboBox">
                        <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Titles}"/>
                        <Setter Property="DisplayMemberPath" Value="TitleText" />
                    </Style>
                </DataGridComboBoxColumn.EditingElementStyle>
            </DataGridComboBoxColumn>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

Person.cs

public class Person
{
    public int TitleId { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Title.cs

public struct Title
{
    public Title(int titleId, string titleText)
        : this()
    {
        TitleId = titleId;
        TitleText = titleText;
    }

    public string TitleText { get; private set; }
    public int TitleId { get; private set; }

    public static List<Title> GetAvailableTitles()
    {
        var titles = new List<Title>();

        titles.Add(new Title(1, "Mr"));
        titles.Add(new Title(2, "Miss"));
        titles.Add(new Title(3, "Mrs"));

        return titles;
    }
}

MainWindowViewModel.cs

public class MainWindowViewModel : ViewModelBase
{
    private ObservableCollection<Person> contacts;
    private List<Title> titles;

    public MainWindowViewModel()
    {
        titles = Title.GetAvailableTitles();

        Contacts = new ObservableCollection<Person>();
        Contacts.Add(new Person() { FirstName = "Jane", LastName = "Smith", TitleId = 2 });
    }

    public List<Title> Titles
    {
        get { return titles; }
    }

    public ObservableCollection<Person> Contacts
    {
        get { return contacts; }
        set
        {
            if (contacts != value)
            {
                contacts = value;
                this.OnPropertyChanged("Contacts");
            }
        }
    }
}

ViewModelBase.cs

public class ViewModelBase : INotifyPropertyChanged
{
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}
Community
  • 1
  • 1
Rob
  • 45,296
  • 24
  • 122
  • 150
  • It would help if you could outline what parts of that 'plan' do not work/which actually do. e.g. are you able to locate the list for the ComboBox's ItemsSource? – H.B. Apr 01 '11 at 19:32
  • Also: From the looks of how you use `Title` the class seems redundant and would be better off being replaced by a `Dictionary`. – H.B. Apr 01 '11 at 19:38
  • @H.B. - As the code stands the grid doesn't show the textual representation of the initial value (i.e. the appropriate TitleText for Person.TitleId), the drop-down gets populated with {Mr,Mrs,Miss} correctly and selecting an item in the drop-down results in TestMVVM.Models.Title being shown in the grid (TestMVVM.Models being the namespace in the solution, that I stripped out for brevity). – Rob Apr 01 '11 at 19:39
  • @H.B. - "Title" is a simplification of my "real" code. I've reduced the *real* code down to this specimen case (in my "real" code, the Title class has more properties and whilst I could distill it down to a `Dictionary` for the purposes of the grid, I'd rather not as it's more code) to see if I could solve it myself without any extraneous issues such as database access. Given that the minimal case is the best one to post on stackoverflow, that's what I've done =) – Rob Apr 01 '11 at 19:41
  • Oh sorry, i did not realize you compressed it already because it is still kind of huge. – H.B. Apr 01 '11 at 19:44
  • @H.B. - the grid actually contains 12 columns and obviously the models are rather different. I've got everything else working bar the `DataGridComboBoxColumn`s. They're rapidly turning into the bane of my life! =) – Rob Apr 01 '11 at 19:47
  • @Rob, I've managed to add a text representation of title, but I still have an issue with `TitleId` not being populated. Do you consider an option of changing `TitleId` into `Title` reference in `Person` class? – Snowbear Apr 01 '11 at 19:50
  • @Snowbear - One option I considered was making the `ObservableCollection` be `` so I could do something like that (`Person` is shared with some other projects so I can't change it), so an answer that does that would work :) – Rob Apr 01 '11 at 20:01

2 Answers2

39

Here is a working code. The key point here was to use SelectedValueBinding instead of SelecteItemBinding.

<DataGridComboBoxColumn Header="Title" 
                        SelectedValueBinding="{Binding TitleId}"
                        SelectedValuePath="TitleId"
                        DisplayMemberPath="TitleText"
                        >
    <DataGridComboBoxColumn.ElementStyle>
        <Style TargetType="ComboBox">
            <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Titles}"/>
        </Style>
    </DataGridComboBoxColumn.ElementStyle>
    <DataGridComboBoxColumn.EditingElementStyle>
        <Style TargetType="ComboBox">
            <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Titles}"/>
        </Style>
    </DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
Snowbear
  • 16,924
  • 3
  • 43
  • 67
  • Unbelievable, i was just writing pretty much the exact same thing, well, this round goes to you. – H.B. Apr 01 '11 at 20:20
  • this works perfectly in my reduced case, I'm going to spend some time integrating it into my actual code to see if it works there (I'm sure it will!). Any chance you could explain *how/why* it works? :) – Rob Apr 01 '11 at 20:36
  • 3
    `SelectedValuePath` sets the path to the member inside the selected object which represents the selected item of the ComboBox, `DisplayMemberPath` on the other hand sets the path to the member inside the class that should be displayed. The `SelectedValueBinding` will bind the selected value to the property specified in the binding. It's a bit confusing but if you use it a few times it makes sense. E: http://stackoverflow.com/questions/3797034/confused-with-wpf-combobox-displaymemberpath-selectedvalue-and-selectedvaluepath – H.B. Apr 01 '11 at 20:48
  • I've integrated it into my "actual" code and it works a treat, thankyou! =) – Rob Apr 02 '11 at 14:52
  • I wish I could upvote this more than once. Thank you for the concise working example, @Snowbear. – Paul Prewett Nov 04 '15 at 18:04
  • THANK YOU, the OP and you save my day! oh it's friday - beer time :) – CeOnSql Jul 01 '16 at 10:52
2

@SnowBear's answer worked well for me. But I want to clarify a detail of the binding.

In @Rob's example, both Title and Person classes use TitleID. Therefore, in @SnowBear's answer, in the binding:

SelectedValueBinding="{Binding TitleId}"

it wasn't immediately obvious to me which class and property was being bound.

Because the SelectedValueBinding attribute appeared on the DataGridComboBoxColumn, it is binding to the ItemsSource of the containing DataGrid. In this case the Contacts collection of Person objects.

In my case, the DataGrid's DataSource collection was attributed with a property that was named different from the ValuePath of the ComboBox's ItemSource collection. So my SelectedValueBinding's value was bound to a different property than the property named in the ComboBox's SelectedValuePath.

Paul Chavez
  • 895
  • 2
  • 13
  • 22