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 DataContext
w 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 ItemsSource
DataGrid na GridItems
kolekcję ViewModel. Ta część działa, wyświetlana jest pojedyncza linia siatki z nazwą „Jim”.
Chcę również ustawić ItemsSource
ComboBox w każdym wierszu na CompanyItems
kolekcję 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ą, GridItem
której nie ma, i to jest powód, dla którego wiązanie zawodzi.
Próbowałem już pracować z RelativeSource
i AncestorType
tak:
<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!
Dokumentacji MSDN o
ItemsSource
zDataGridComboBoxColumn
mówi, że tylko zasoby statyczne, statyczne lub kod inline zbiory elementów ComboBox mogą być związane zItemsSource
:Powiązanie z właściwością DataContext nie jest możliwe, jeśli dobrze to rozumiem.
I rzeczywiście: Kiedy robię
CompanyItems
się 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ę.
źródło
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 tworzenie
CollectionViewSource
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).
źródło
MyItems
doprowadziłoby do kompilacji błędu, jeżeli są stosowane z kodem OPZdaję 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>
źródło
RookieRick ma rację, używając
DataGridTemplateColumn
zamiastDataGridComboBoxColumn
daje znacznie prostszy XAML.Co więcej, umieszczenie
CompanyItem
listy bezpośrednio dostępnej z poziomuGridItem
pozwala pozbyć się plikuRelativeSource
.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; } }
źródło
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.CompanyItems
ponieważ Window.CompanyItems nie istniejeźródło
CompanyItems
przezDataContext.CompanyItems
w ostatnim fragmencie kodu XAML w moim pytaniu), ale daje mi ten sam błąd w danych wyjściowych debugera.{Binding ElementName=RootWindow, Path=DataContext.CompanyItems}
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>
źródło