Enlazar ItemsSource de un ComboBoxColumn en WPF DataGrid

82

Tengo dos clases de modelo simples y un 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; }
}

... y una simple ventana:

<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 se establece en MainWindow DataContexten 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();
    }
}

Como puede ver, configuré ItemsSourceel DataGrid en la GridItemscolección de ViewModel. Esta parte funciona, se muestra la línea de cuadrícula única con el nombre "Jim".

También quiero establecer el ItemsSourceComboBox en cada fila en la CompanyItemscolección de ViewModel. Esta parte no funciona: el ComboBox permanece vacío y en la ventana de salida del depurador veo un mensaje de error:

Error de System.Windows.Data: 2: No se puede encontrar FrameworkElement o FrameworkContentElement para el elemento de destino. BindingExpression: Path = CompanyItems; DataItem = null; el elemento de destino es 'DataGridComboBoxColumn' (HashCode = 28633162); la propiedad de destino es 'ItemsSource' (tipo 'IEnumerable')

Creo que WPF espera CompanyItemsser una propiedad de la GridItemcual no es el caso, y esa es la razón por la que falla el enlace.

Ya intenté trabajar con a RelativeSourcey AncestorTypeasí:

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

Pero eso me da otro error en la salida del depurador:

Error de System.Windows.Data: 4: No se puede encontrar la fuente para el enlace con la referencia 'RelativeSource FindAncestor, AncestorType =' System.Windows.Window ', AncestorLevel =' 1 ''. BindingExpression: Path = CompanyItems; DataItem = null; el elemento de destino es 'DataGridComboBoxColumn' (HashCode = 1150788); la propiedad de destino es 'ItemsSource' (tipo 'IEnumerable')

Pregunta: ¿Cómo puedo vincular ItemsSource de DataGridComboBoxColumn a la colección CompanyItems de ViewModel? ¿Es posible en absoluto?

¡Gracias por la ayuda de antemano!

Slauma
fuente

Respuestas:

123

Por favor, compruebe si DataGridComboBoxColumn xaml a continuación funcionaría para usted:

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

Aquí puede encontrar otra solución para el problema al que se enfrenta: Uso de cuadros combinados con WPF DataGrid

serge_gubenko
fuente
4
Demonios, esto funciona !!! Si tan solo pudiera entender por qué ¿Y por qué no el código original con los cambios que recomendó Rachel? ¡De todos modos, muchas gracias!
Slauma
1
Creo que puede encontrar la explicación aquí: wpf.codeplex.com/workitem/8153?ProjectName=wpf (ver comentarios)
serge_gubenko
1
Parece que han decidido convertir este error ("Hemos archivado un error en nuestra base de datos interna para corregirlo en una versión futura") en una función. Eche un vistazo a mi propia respuesta en este hilo: el problema ha sido resuelto por la documentación, una fuerte indicación de que esto nunca cambiará.
Slauma
1
+1 para el enlace joemorrison.org/blog/2009/02/17/… . eso resolvió mi problema. Apesta, ~ 5 horas y me di cuenta de que ya tenía este tipo en mi proyecto para otra cosa que estábamos haciendo :( Siempre es un proceso de aprendizaje.
TravisWhidden
No me funciona. El EditingElementStyle parece funcionar, pero por alguna razón una vez que agrego ElementStyle obtengo ComboBoxes que no completan nada (en lugar del valor de DisplayMemberPath), y no volverá al EditingElementStyle cuando hago clic.
William
46

La documentación de MSDN sobre la ItemsSourcedeDataGridComboBoxColumn dice que solo los recursos estáticos, el código estático o las colecciones en línea de elementos de cuadro combinado se pueden vincular a ItemsSource:

Para completar la lista desplegable, primero establezca la propiedad ItemsSource para el ComboBox usando una de las siguientes opciones:

  • Un recurso estático. Para obtener más información, consulte Extensión de marcado StaticResource.
  • Una x: entidad de código estático. Para obtener más información, consulte x: Static Markup Extension.
  • Una colección en línea de tipos ComboBoxItem.

La vinculación a la propiedad de un DataContext no es posible si lo entiendo correctamente.

Y de hecho: cuando hago CompanyItemsuna propiedad estática en ViewModel ...

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

... agregue el espacio de nombres donde se encuentra ViewModel a la ventana ...

xmlns:vm="clr-namespace:DataGridComboBoxColumnApp"

... y cambie el enlace a ...

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

... entonces funciona. Pero tener ItemsSource como una propiedad estática a veces puede estar bien, pero no siempre es lo que quiero.

Slauma
fuente
1
todavía espero que microsoft solucione este error
juFo
38

La solución correcta parece ser:

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

El diseño anterior funciona perfectamente bien para mí y debería funcionar para otros. Esta elección de diseño también tiene sentido, aunque no se explica muy bien en ninguna parte. Pero si tiene una columna de datos con valores predefinidos, esos valores normalmente no cambian durante el tiempo de ejecución. Por lo tanto, tiene sentido crear CollectionViewSourcee inicializar los datos una vez. También se deshace de los enlaces más largos para encontrar un ancestro y enlazar su contexto de datos (que siempre me pareció mal).

Dejo esto aquí para cualquier otra persona que haya tenido problemas con este enlace y se preguntó si había una mejor manera (como esta página obviamente todavía aparece en los resultados de búsqueda, así es como llegué aquí).

Adam Becker
fuente
1
Aunque podría decirse que es una buena respuesta, quizás se abstraiga de la pregunta del OP. Su MyItemsdaría lugar a un error de compilación si se utiliza con el código de la OP
MickyD
22

Me doy cuenta de que esta pregunta tiene más de un año, pero me encontré con ella al tratar con un problema similar y pensé que compartiría otra posible solución en caso de que pudiera ayudar a un futuro viajero (oa mí mismo, cuando me olvide de esto más tarde y me encuentre dando vueltas en StackOverflow entre gritos y lanzamientos del objeto más cercano en mi escritorio).

En mi caso, pude obtener el efecto que quería usando un DataGridTemplateColumn en lugar de un DataGridComboBoxColumn, al estilo del siguiente fragmento. [advertencia: estoy usando .NET 4.0, y lo que he estado leyendo me lleva a creer que DataGrid ha evolucionado mucho, por lo que YMMV si usa una versión anterior]

<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
fuente
Después de luchar con las primeras respuestas, probé esto y también funcionó para mí. Gracias.
coson
7

RookieRick tiene razón, usar en DataGridTemplateColumnlugar de DataGridComboBoxColumnofrece un XAML mucho más simple.

Además, poner la CompanyItemlista directamente accesible desde le GridItempermite deshacerse del RelativeSource.

En mi humilde opinión, esto te da una solución muy limpia.

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>

Ver modelo:

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

Su ComboBox está intentando enlazar para enlazar GridItem[x].CompanyItems, lo cual no existe.

Su RelativeBinding está cerca, sin embargo, debe vincularse DataContext.CompanyItemsporque Window.CompanyItems no existe

Raquel
fuente
¡Gracias por responder! Lo intenté (reemplazado CompanyItemspor DataContext.CompanyItemsen el último fragmento de XAML en mi pregunta) pero me da el mismo error en la salida del depurador.
Slauma
1
@Slauma No estoy seguro entonces, debería funcionar. Lo único inusual que veo con el XAML que tienes es el Mode = FindAncestor y generalmente lo omito. ¿Ha intentado darle un nombre a su ventana raíz y hacer referencia a ella por nombre en su enlace en lugar de usar RelativeSource? {Binding ElementName=RootWindow, Path=DataContext.CompanyItems}
Rachel
Probé ambas cosas (omití Mode = FindAncestor y cambié el enlace a un elemento con nombre), pero no funciona. Es extraño que esta forma funcione para ti. He creado esta sencilla aplicación de prueba para arrastrar el problema fuera de mi aplicación a un contexto muy simple. No sé qué podría hacer mal, el código que ve en la pregunta es la aplicación completa (creada a partir de la plantilla de proyecto WPF en VS2010), no hay nada más alrededor de este código.
Slauma
1

la forma en que utilizo el bastidor, enlazo el bloque de texto y el cuadro combinado a la misma propiedad y esta propiedad debería ser compatible con notifyPropertyChanged.

Utilicé un recurso relativo para unirme al contexto de datos de la vista principal, que es el control del usuario para subir el nivel de la cuadrícula de datos en el enlace porque en este caso la cuadrícula de datos buscará en el objeto que usó en la cuadrícula de datos.

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