Binding ItemsSource of a ComboBoxColumn w WPF DataGrid

82

Mam dwie proste klasy Model i ViewModel ...

public class GridItem
{
    public string Name { get; set; }
    public int CompanyID { get; set; }
}

public class CompanyItem
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class ViewModel
{
    public ViewModel()
    {
        GridItems = new ObservableCollection<GridItem>() {
            new GridItem() { Name = "Jim", CompanyID = 1 } };

        CompanyItems = new ObservableCollection<CompanyItem>() {
            new CompanyItem() { ID = 1, Name = "Company 1" },
            new CompanyItem() { ID = 2, Name = "Company 2" } };
    }

    public ObservableCollection<GridItem> GridItems { get; set; }
    public ObservableCollection<CompanyItem> CompanyItems { get; set; }
}

... i proste okno:

<Window x:Class="DataGridComboBoxColumnApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GridItems}" >
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" />
                <DataGridComboBoxColumn ItemsSource="{Binding CompanyItems}"
                                    DisplayMemberPath="Name"
                                    SelectedValuePath="ID"
                                    SelectedValueBinding="{Binding CompanyID}" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

ViewModel jest ustawiony na MainWindow DataContextw App.xaml.cs:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        MainWindow window = new MainWindow();
        ViewModel viewModel = new ViewModel();

        window.DataContext = viewModel;
        window.Show();
    }
}

Jak widać, ustawiłem ItemsSourceDataGrid na GridItemskolekcję ViewModel. Ta część działa, wyświetlana jest pojedyncza linia siatki z nazwą „Jim”.

Chcę również ustawić ItemsSourceComboBox w każdym wierszu na CompanyItemskolekcję ViewModel. Ta część nie działa: ComboBox pozostaje pusty iw oknie danych wyjściowych debugera widzę komunikat o błędzie:

Błąd System.Windows.Data: 2: nie można znaleźć regulującego elementu FrameworkElement lub FrameworkContentElement dla elementu docelowego. BindingExpression: Path = CompanyItems; DataItem = null; element docelowy to „DataGridComboBoxColumn” (HashCode = 28633162); Właściwość docelowa to „ItemsSource” (typ „IEnumerable”)

Uważam, że WPF oczekuje, CompanyItemsże będzie właściwością, GridItemktórej nie ma, i to jest powód, dla którego wiązanie zawodzi.

Próbowałem już pracować z RelativeSourcei AncestorTypetak:

<DataGridComboBoxColumn ItemsSource="{Binding CompanyItems, 
    RelativeSource={RelativeSource Mode=FindAncestor,
                                   AncestorType={x:Type Window}}}"
                        DisplayMemberPath="Name"
                        SelectedValuePath="ID"
                        SelectedValueBinding="{Binding CompanyID}" />

Ale to daje mi kolejny błąd w danych wyjściowych debugera:

Błąd System.Windows.Data: 4: nie można znaleźć źródła powiązania z odwołaniem „RelativeSource FindAncestor, AncestorType = 'System.Windows.Window', AncestorLevel = '1'. BindingExpression: Path = CompanyItems; DataItem = null; element docelowy to „DataGridComboBoxColumn” (HashCode = 1150788); Właściwość docelowa to „ItemsSource” (typ „IEnumerable”)

Pytanie: Jak mogę powiązać ItemsSource DataGridComboBoxColumn z kolekcją CompanyItems ViewModel? Czy to w ogóle możliwe?

Z góry dziękuję za pomoc!

Slauma
źródło

Odpowiedzi:

123

Pls, sprawdź, czy DataGridComboBoxColumn xaml poniżej będzie działać dla Ciebie:

<DataGridComboBoxColumn 
    SelectedValueBinding="{Binding CompanyID}" 
    DisplayMemberPath="Name" 
    SelectedValuePath="ID">

    <DataGridComboBoxColumn.ElementStyle>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="ItemsSource" Value="{Binding Path=DataContext.CompanyItems, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </Style>
    </DataGridComboBoxColumn.ElementStyle>
    <DataGridComboBoxColumn.EditingElementStyle>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="ItemsSource" Value="{Binding Path=DataContext.CompanyItems, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </Style>
    </DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>

Tutaj możesz znaleźć inne rozwiązanie problemu, z którym się spotykasz: Używanie pól kombi z WPF DataGrid

serge_gubenko
źródło
4
Do diabła, to działa !!! Gdybym tylko mógł zrozumieć, dlaczego? A dlaczego nie oryginalny kod ze zmianami zalecanymi przez Rachel? W każdym razie bardzo dziękuję!
Slauma
1
Myślę, że możesz znaleźć wyjaśnienie tutaj: wpf.codeplex.com/workitem/8153?ProjectName=wpf (patrz komentarze)
serge_gubenko
1
Wydaje się, że zdecydowali się zmienić ten błąd („Znieśliśmy błąd w naszej wewnętrznej bazie danych, aby naprawić go w przyszłym wydaniu”) w funkcję. Spójrz na moją własną odpowiedź w tym wątku: Problem został rozwiązany dzięki dokumentacji, co jest mocną wskazówką, że nigdy nie zostanie to zmienione.
Slauma,
1
+1 dla linku joemorrison.org/blog/2009/02/17/… . to rozwiązało mój problem. Sucks, ~ 5 godzin i zdałem sobie sprawę, że miałem już ten typ w moim projekcie z powodu czegoś innego, co robiliśmy :( To zawsze jest proces uczenia się.
TravisWhidden
Nie działa na mnie. Wydaje się, że EditingElementStyle działa, ale z jakiegoś powodu po dodaniu ElementStyle otrzymuję ComboBoxes, które nie wypełniają niczego (zamiast wartości z DisplayMemberPath) i nie przełączają się z powrotem do EditingElementStyle po kliknięciu.
William
46

Dokumentacji MSDN o ItemsSourcezDataGridComboBoxColumn mówi, że tylko zasoby statyczne, statyczne lub kod inline zbiory elementów ComboBox mogą być związane z ItemsSource:

Aby wypełnić listę rozwijaną, najpierw ustaw właściwość ItemsSource dla ComboBox przy użyciu jednej z następujących opcji:

  • Zasób statyczny. Aby uzyskać więcej informacji, zobacz rozszerzenie znaczników StaticResource.
  • X: Statyczna jednostka kodu. Aby uzyskać więcej informacji, zobacz x: Static Markup Extension.
  • Wbudowana kolekcja typów ComboBoxItem.

Powiązanie z właściwością DataContext nie jest możliwe, jeśli dobrze to rozumiem.

I rzeczywiście: Kiedy robię CompanyItemssię statyczną właściwość w ViewModel ...

public static ObservableCollection<CompanyItem> CompanyItems { get; set; }

... dodaj do okna przestrzeń nazw, w której znajduje się ViewModel ...

xmlns:vm="clr-namespace:DataGridComboBoxColumnApp"

... i zmień wiązanie na ...

<DataGridComboBoxColumn
    ItemsSource="{Binding Source={x:Static vm:ViewModel.CompanyItems}}" 
    DisplayMemberPath="Name"
    SelectedValuePath="ID"
    SelectedValueBinding="{Binding CompanyID}" />

... wtedy to działa. Ale posiadanie ItemsSource jako właściwości statycznej może czasami być OK, ale nie zawsze jest to, czego chcę.

Slauma
źródło
1
wciąż mam nadzieję, że Microsoft naprawi ten błąd
juFo
38

Wydaje się, że właściwym rozwiązaniem jest:

<Window.Resources>
    <CollectionViewSource x:Key="ItemsCVS" Source="{Binding MyItems}" />
</Window.Resources>
<!-- ... -->
<DataGrid ItemsSource="{Binding MyRecords}">
    <DataGridComboBoxColumn Header="Column With Predefined Values"
                            ItemsSource="{Binding Source={StaticResource ItemsCVS}}"
                            SelectedValueBinding="{Binding MyItemId}"
                            SelectedValuePath="Id"
                            DisplayMemberPath="StatusCode" />
</DataGrid>

Powyższy układ działa idealnie dla mnie i powinien działać dla innych. Ten wybór projektu również ma sens, chociaż nigdzie nie jest dobrze wyjaśniony. Ale jeśli masz kolumnę danych ze wstępnie zdefiniowanymi wartościami, te wartości zwykle nie zmieniają się w czasie wykonywania. Więc tworzenieCollectionViewSource i jednorazowa inicjalizacja danych ma sens. Pozbywa się również dłuższych powiązań, aby znaleźć przodka i powiązać go z jego kontekstem danych (co zawsze wydawało mi się złe).

Zostawiam to tutaj dla każdego, kto zmagał się z tym wiązaniem i zastanawiał się, czy istnieje lepszy sposób (ponieważ ta strona oczywiście wciąż pojawia się w wynikach wyszukiwania, tak się tutaj znalazłem).

Adam Becker
źródło
1
Chociaż prawdopodobnie dobra odpowiedź, może być wyodrębniona z pytania PO. Twój MyItemsdoprowadziłoby do kompilacji błędu, jeżeli są stosowane z kodem OP
MickyD
22

Zdaję sobie sprawę, że to pytanie ma ponad rok, ale właśnie natknąłem się na nie, mając do czynienia z podobnym problemem i pomyślałem, że podzielę się innym potencjalnym rozwiązaniem na wypadek, gdyby mogło to pomóc przyszłemu podróżnikowi (lub sobie, kiedy zapomnę o tym później i znajdę flopowanie na StackOverflow między krzykiem a rzucaniem najbliższym przedmiotem na moim biurku).

W moim przypadku udało mi się uzyskać efekt, który chciałem, używając DataGridTemplateColumn zamiast DataGridComboBoxColumn, a la poniższy fragment. [uwaga: używam .NET 4.0 i to, co czytałem, prowadzi mnie do przekonania, że ​​DataGrid bardzo się rozwinął, więc YMMV, jeśli używam wcześniejszej wersji]

<DataGridTemplateColumn Header="Identifier_TEMPLATED">
    <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <ComboBox IsEditable="False" 
                Text="{Binding ComponentIdentifier,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
                ItemsSource="{Binding Path=ApplicableIdentifiers, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding ComponentIdentifier}" />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Rick Riensche
źródło
Po zmaganiu się z kilkoma pierwszymi odpowiedziami spróbowałem tego i również zadziałało. Dzięki.
coson
7

RookieRick ma rację, używając DataGridTemplateColumnzamiast DataGridComboBoxColumndaje znacznie prostszy XAML.

Co więcej, umieszczenie CompanyItemlisty bezpośrednio dostępnej z poziomu GridItempozwala pozbyć się pliku RelativeSource.

IMHO, to daje bardzo czyste rozwiązanie.

XAML:

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GridItems}" >
    <DataGrid.Resources>
        <DataTemplate x:Key="CompanyDisplayTemplate" DataType="vm:GridItem">
            <TextBlock Text="{Binding Company}" />
        </DataTemplate>
        <DataTemplate x:Key="CompanyEditingTemplate" DataType="vm:GridItem">
            <ComboBox SelectedItem="{Binding Company}" ItemsSource="{Binding CompanyList}" />
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Name}" />
        <DataGridTemplateColumn CellTemplate="{StaticResource CompanyDisplayTemplate}"
                                CellEditingTemplate="{StaticResource CompanyEditingTemplate}" />
    </DataGrid.Columns>
</DataGrid>

Zobacz model:

public class GridItem
{
    public string Name { get; set; }
    public CompanyItem Company { get; set; }
    public IEnumerable<CompanyItem> CompanyList { get; set; }
}

public class CompanyItem
{
    public int ID { get; set; }
    public string Name { get; set; }

    public override string ToString() { return Name; }
}

public class ViewModel
{
    readonly ObservableCollection<CompanyItem> companies;

    public ViewModel()
    {
        companies = new ObservableCollection<CompanyItem>{
            new CompanyItem { ID = 1, Name = "Company 1" },
            new CompanyItem { ID = 2, Name = "Company 2" }
        };

        GridItems = new ObservableCollection<GridItem> {
            new GridItem { Name = "Jim", Company = companies[0], CompanyList = companies}
        };
    }

    public ObservableCollection<GridItem> GridItems { get; set; }
}
Benoit Blanchon
źródło
4

Twój ComboBox próbuje się połączyć, aby się z nim powiązać GridItem[x].CompanyItems, co nie istnieje.

Twoje RelativeBinding jest blisko, jednak musi zostać powiązane, DataContext.CompanyItemsponieważ Window.CompanyItems nie istnieje

Rachel
źródło
Dzięki za odpowiedź! Próbowałem tego (zastąpione CompanyItemsprzez DataContext.CompanyItemsw ostatnim fragmencie kodu XAML w moim pytaniu), ale daje mi ten sam błąd w danych wyjściowych debugera.
Slauma
1
@Slauma Nie jestem więc pewien, powinno działać. Jedyną niezwykłą rzeczą, jaką widzę w XAML, który masz, jest Mode = FindAncestor i zwykle to pomijam. Czy próbowałeś nadać swojemu oknu głównemu nazwę i odwoływać się do niego przez nazwę w swoim powiązaniu zamiast używać RelativeSource? {Binding ElementName=RootWindow, Path=DataContext.CompanyItems}
Rachel
Próbowałem obu rzeczy (pominięto Mode = FindAncestor i zmieniono powiązanie z nazwanym elementem), ale to nie działa. Dziwne, że ten sposób Ci odpowiada. Stworzyłem tę prostą aplikację testową, aby przenieść problem z mojej aplikacji do bardzo prostego kontekstu. Nie wiem, co mogłem zrobić źle, kod, który widzisz w pytaniu to pełna aplikacja (utworzona z szablonu projektu WPF w VS2010), wokół tego kodu nie ma nic więcej.
Slauma
1

bastowy sposób, w jaki używam, wiążę textblock i combobox z tą samą właściwością, a ta właściwość powinna obsługiwać notifyPropertyChanged.

Użyłem zasobu względnego do powiązania z kontekstem danych widoku nadrzędnego, który jest kontrolowany przez użytkownika, aby przejść do wyższego poziomu datagrid w powiązaniu, ponieważ w tym przypadku datagrid będzie wyszukiwać w obiekcie, którego użyłeś w datagrid.itemsource

<DataGridTemplateColumn Header="your_columnName">
     <DataGridTemplateColumn.CellTemplate>
          <DataTemplate>
             <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.SelectedUnit.Name, Mode=TwoWay}" />
           </DataTemplate>
     </DataGridTemplateColumn.CellTemplate>
     <DataGridTemplateColumn.CellEditingTemplate>
           <DataTemplate>
            <ComboBox DisplayMemberPath="Name"
                      IsEditable="True"
                      ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.UnitLookupCollection}"
                       SelectedItem="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.SelectedUnit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                      SelectedValue="{Binding UnitId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                      SelectedValuePath="Id" />
            </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
Hisham
źródło