Binding ItemsSource einer ComboBoxColumn in WPF DataGrid

80

Ich habe zwei einfache Modellklassen und ein 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; }
}

... und ein einfaches Fenster:

<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>

Das ViewModel ist DataContextin App.xaml.cs auf das MainWindow eingestellt:

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();
    }
}

Wie Sie sehen, setze ich ItemsSourcedas DataGrid auf die GridItemsSammlung des ViewModel. Dieser Teil funktioniert, die einzelne Gitterlinie mit dem Namen "Jim" wird angezeigt.

Ich möchte auch ItemsSourcedie ComboBox in jeder Zeile auf die CompanyItemsSammlung des ViewModel setzen. Dieser Teil funktioniert nicht: Die ComboBox bleibt leer und im Debugger-Ausgabefenster wird eine Fehlermeldung angezeigt:

System.Windows.Data-Fehler: 2: FrameworkElement oder FrameworkContentElement für das Zielelement können nicht gefunden werden. BindingExpression: Path = CompanyItems; DataItem = null; Zielelement ist 'DataGridComboBoxColumn' (HashCode = 28633162); Die Zieleigenschaft ist 'ItemsSource' (Typ 'IEnumerable').

Ich glaube, dass WPF erwartet CompanyItems, eine Eigenschaft zu sein, GridItemdie nicht der Fall ist, und das ist der Grund, warum die Bindung fehlschlägt.

Ich habe bereits versucht mit einem zu arbeiten RelativeSourceund AncestorTypemag so:

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

Aber das gibt mir einen weiteren Fehler in der Debugger-Ausgabe:

System.Windows.Data-Fehler: 4: Quelle für Bindung mit Referenz 'RelativeSource FindAncestor, AncestorType =' System.Windows.Window ', AncestorLevel =' 1 '' kann nicht gefunden werden. BindingExpression: Path = CompanyItems; DataItem = null; Zielelement ist 'DataGridComboBoxColumn' (HashCode = 1150788); Die Zieleigenschaft ist 'ItemsSource' (Typ 'IEnumerable').

Frage: Wie kann ich die ItemsSource der DataGridComboBoxColumn an ​​die CompanyItems-Auflistung des ViewModel binden? Ist das überhaupt möglich?

Vielen Dank für Ihre Hilfe im Voraus!

Slauma
quelle

Antworten:

119

Bitte überprüfen Sie, ob DataGridComboBoxColumn xaml unten für Sie funktionieren würde:

<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>

Hier finden Sie eine andere Lösung für das Problem, mit dem Sie konfrontiert sind: Verwenden von Kombinationsfeldern mit dem WPF DataGrid

serge_gubenko
quelle
4
Hölle, das funktioniert !!! Wenn ich nur verstehen könnte warum? Und warum nicht den Originalcode mit den von Rachel empfohlenen Änderungen? Trotzdem vielen Dank!
Slauma
1
Ich glaube, Sie können die Erklärung hier finden: wpf.codeplex.com/workitem/8153?ProjectName=wpf (siehe Kommentare)
serge_gubenko
Sie scheinen beschlossen zu haben, diesen Fehler ("Wir haben einen Fehler in unserer internen Datenbank abgelegt, der in einer zukünftigen Version behoben werden soll.") In eine Funktion umzuwandeln. Schauen Sie sich meine eigene Antwort in diesem Thread an: Das Problem wurde durch Dokumentation gelöst, ein starkes Indiz dafür, dass dies niemals geändert wird.
Slauma
1
+1 für den Link joemorrison.org/blog/2009/02/17/… . das hat mein Problem gelöst. Scheiße, ~ 5 Stunden und mir wurde klar, dass ich diesen Typ bereits in meinem Projekt für etwas anderes hatte, das wir machten :( Es ist immer ein Lernprozess.
TravisWhidden
Funktioniert bei mir nicht Der EditingElementStyle scheint zu funktionieren, aber aus irgendeinem Grund erhalte ich beim Hinzufügen von ElementStyle ComboBoxen, die nichts füllen (anstelle des Werts von DisplayMemberPath), und er wechselt nicht zum EditingElementStyle zurück, wenn ich auf klicke.
William
46

Die Dokumentation auf MSDN über die ItemsSourcevon derDataGridComboBoxColumn sagt , dass nur statische Ressourcen, statische Code oder Inline - Sammlungen von Combobox Elemente können an die gebunden werden ItemsSource:

Um die Dropdown-Liste zu füllen, legen Sie zunächst die ItemsSource-Eigenschaft für die ComboBox fest, indem Sie eine der folgenden Optionen verwenden:

  • Eine statische Ressource. Weitere Informationen finden Sie unter StaticResource Markup Extension.
  • Eine x: Statische Code-Entität. Weitere Informationen finden Sie unter x: Static Markup Extension.
  • Eine Inline-Sammlung von ComboBoxItem-Typen.

Das Binden an die Eigenschaft eines DataContext ist nicht möglich, wenn ich das richtig verstehe.

Und tatsächlich: Wenn ich CompanyItemseine statische Eigenschaft in ViewModel erstelle ...

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

... fügen Sie dem Fenster den Namespace hinzu, in dem sich das ViewModel befindet ...

xmlns:vm="clr-namespace:DataGridComboBoxColumnApp"

... und ändern Sie die Bindung in ...

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

... dann funktioniert es. Aber die ItemsSource als statische Eigenschaft zu haben, mag manchmal in Ordnung sein, aber es ist nicht immer das, was ich will.

Slauma
quelle
1
Ich hoffe immer noch, dass Microsoft diesen Fehler beheben wird
am
36

Die richtige Lösung scheint zu sein:

<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>

Das obige Layout funktioniert für mich einwandfrei und sollte für andere funktionieren. Diese Designauswahl ist ebenfalls sinnvoll, obwohl sie nirgendwo sehr gut erklärt wird. Wenn Sie jedoch eine Datenspalte mit vordefinierten Werten haben, ändern sich diese Werte normalerweise zur Laufzeit nicht. Das CollectionViewSourceeinmalige Erstellen und Initialisieren der Daten ist also sinnvoll. Es werden auch die längeren Bindungen beseitigt, um einen Vorfahren zu finden und seinen Datenkontext zu binden (was sich für mich immer falsch anfühlte).

Ich überlasse dies hier allen anderen, die mit dieser Bindung zu kämpfen haben und sich gefragt haben, ob es einen besseren Weg gibt (da diese Seite offensichtlich immer noch in den Suchergebnissen auftaucht, bin ich hierher gekommen).

Adam Becker
quelle
1
Obwohl dies wohl eine gute Antwort ist, wird sie möglicherweise von der Frage des OP abstrahiert . Sie MyItemswürden zu einem Kompilierungsfehler führen, wenn Sie ihn mit dem OP-Code verwenden
MickyD
22

Mir ist klar, dass diese Frage über ein Jahr alt ist, aber ich bin bei der Behandlung eines ähnlichen Problems nur darauf gestoßen und dachte, ich würde eine andere mögliche Lösung teilen, falls sie einem zukünftigen Reisenden (oder mir selbst, wenn ich dies später vergesse und mich selbst finde) helfen könnte auf StackOverflow herumflippen zwischen Schreien und Werfen des nächsten Objekts auf meinem Schreibtisch).

In meinem Fall konnte ich den gewünschten Effekt erzielen, indem ich eine DataGridTemplateColumn anstelle einer DataGridComboBoxColumn verwendete, wie im folgenden Snippet beschrieben. [Vorbehalt: Ich verwende .NET 4.0, und was ich gelesen habe, lässt mich glauben, dass sich das DataGrid stark weiterentwickelt hat, also YMMV, wenn frühere Versionen verwendet werden]

<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
quelle
Nachdem ich mit den ersten Antworten zu kämpfen hatte, versuchte ich es und es funktionierte auch für mich. Vielen Dank.
Coson
7

RookieRick hat recht, DataGridTemplateColumnstatt DataGridComboBoxColumneine viel einfachere XAML zu verwenden.

Wenn Sie die CompanyItemListe direkt von der GridItemWebsite aus aufrufen, können Sie sie außerdem entfernen RelativeSource.

IMHO, das gibt Ihnen eine sehr saubere Lösung.

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>

Modell anzeigen:

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
quelle
4

Ihre ComboBox versucht zu binden, um zu binden GridItem[x].CompanyItems, was nicht existiert.

Ihre RelativeBinding ist geschlossen, muss jedoch DataContext.CompanyItemsgebunden werden , da Window.CompanyItems nicht vorhanden ist

Rachel
quelle
Danke für die Antwort! Ich habe das versucht (ersetzt CompanyItemsdurch DataContext.CompanyItemsdas letzte XAML-Snippet in meiner Frage), aber es gibt mir den gleichen Fehler in der Debugger-Ausgabe.
Slauma
1
@Slauma Ich bin mir dann nicht sicher, es sollte funktionieren. Das einzige, was ich bei der XAML ungewöhnlich sehe, ist der Modus = FindAncestor, und das lasse ich normalerweise weg. Haben Sie versucht, Ihrem Stammfenster einen Namen zu geben und ihn in Ihrer Bindung nach Namen zu referenzieren, anstatt RelativeSource zu verwenden? {Binding ElementName=RootWindow, Path=DataContext.CompanyItems}
Rachel
Habe beide Dinge ausprobiert (Mode = FindAncestor weggelassen und die Bindung in ein benanntes Element geändert), aber es funktioniert nicht. Seltsam, dass dieser Weg für Sie funktioniert. Ich habe diese einfache Testanwendung erstellt, um das Problem aus meiner Anwendung in einen sehr einfachen Kontext zu ziehen. Ich weiß nicht, was ich falsch machen könnte. Der Code, den Sie in der Frage sehen, ist die vollständige Anwendung (erstellt aus der WPF-Projektvorlage in VS2010). Dieser Code enthält nichts mehr.
Slauma
1

Die Bast-Art, die ich benutze, binde ich den Textblock und die Combobox an dieselbe Eigenschaft und diese Eigenschaft sollte notifyPropertyChanged unterstützen.

Ich habe relativeresource verwendet, um an den Datenkontext der übergeordneten Ansicht zu binden. Dies ist eine Benutzersteuerung, um die Datagrid-Ebene beim Binden zu erhöhen, da in diesem Fall das Datagrid in einem Objekt sucht, das Sie in datagrid.itemsource verwendet haben

<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
quelle