使用 MVVM 在 DataGrid 中级联 ComboBox
Cascading ComboBox in DataGrid with MVVM
我的目标是在 WPF 中拥有一组级联组合框。我正在尝试使用 MVVM 模型,但仍在学习中。
项目的一些背景信息。我正在尝试为员工编辑时间。
所以我在 DataGrid 中有一个所选员工的时间列表。 DataGrid 中的每一行都是一个时间对象。时间由一些字段 InTime、OutTime、Date、Hours 等组成。 A Time 也有 Department 和 Job。
目前我已经连接好 Department ComboBox 并正常工作,但我不确定如何根据在 Department 字段中选择的内容构建 Job 组合框。
这是我的 ViewModel 的设置方式
public ObservableCollection<Time> Times { get; set; }
public ObservableCollection<Department> Departments { get; set; }
public TimeSheetsViewModel()
{
Times = new ObservableCollection<Time>();
Departments = new ObservableCollection<Departments>();
GetDepartments();
}
private void GetDepartments()
{
/*
This section contains code to connect to my SQL Database and fills a DataTable dt
*/
if (Departments != null)
Departments.Clear();
for (int i = 0; i < dt.Rows.Count; i++)
{
Department d = new Department() { Display = dt.Rows[i]["DISPLAY"].ToString(), DepartmentCode = dt.Rows[i]["DEPARTMENT_CODE"].ToString(), CompanyCode = dt.Rows[i]["COMPANY_CODE"].ToString() };
Departments.Add(d);
}
}
这是我的 DataGrid 上的绑定
<DataGrid Grid.Row="1" Margin="15,0,15,15" Visibility="Visible" FontSize="14" HorizontalGridLinesBrush="{StaticResource Nelson2}" VerticalGridLinesBrush="{StaticResource Nelson2}" ItemsSource="{Binding Times}" SelectionMode="Single" CellEditEnding="DataGrid_CellEditEnding" RowEditEnding="DataGrid_RowEditEnding" AutoGenerateColumns="False">
<DataGridTemplateColumn Header="Department Code">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path= Department.Display}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type UserControl}}, Path=DataContext.Departments}" DisplayMemberPath="Display" SelectedValuePath="DepartmentCode" SelectedValue="{Binding Department.DepartmentCode}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid>
那么我如何实现我的工作组合框以根据为该行中的部门选择的内容填充其项目?
我假设我想将此代码放在我的同一视图模型中。
感谢任何帮助,谢谢!
编辑 (04/05/16):
如何 return 带有转换器的对象,以便我可以使用该转换器将不同的东西绑定到该对象的字段。
说这是我的转换器
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string departmentCode = values[0].ToString();
ObservableCollection<Department> Departments = values[1] as ObservableCollection<Department>;
return Departments.FirstOrDefault(Department => Department.DepartmentCode == departmentCode);
}
这是我的绑定
<TextBlock >
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DeptCodeToDeptConverter}" >
<Binding Path="DepartmentCode" />
<Binding Path="DataContext.Departments" RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
该转换器将 return 部门对象,但如果我希望 TextBlock 的文本为 Department.Name 或 Department.Location 怎么办?我是否必须为 return 每个要在不同控件中使用的字段创建一个新转换器?或者有没有办法用这个方法达到我想要的效果?
我会选择以下两种方法之一:
1.使用多绑定转换器。 您的第一个组合框的 ItemsSource 将绑定到它的事物集合。第二个可以在第一个的 SelectedItem 和第二个组合框的一些可用项目集集合上使用多绑定转换器,到 return 第二个 ItemsSource 的集合。
当第一个组合框更改它的选定项时,绑定将更新:
public class DepartmentJobComboboValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Department department = values[0] as Department;
ObservableCollection<string> jobCodes = values[1] as ObservableCollection<string>;
//do our logic to filter the job codes by department
return jobCodes.Where(jobCode => jobCode.StartsWith(department.DepartmentCode)).ToList();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
然后您可以将 SelectedItems 绑定为第一个值,将集合字典绑定为第二个值:
<DataGrid ItemsSource="{Binding Times}"
SelectionMode="Single"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Department">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path= Department.DepartmentCode}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type UserControl}}, Path=DataContext.Departments, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="DepartmentCode"
SelectedValuePath="DepartmentCode"
SelectedValue="{Binding Department.DepartmentCode}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Job code">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Job}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox SelectedValue="{Binding Job}">
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource DepartmentJobComboboValueConverter}">
<Binding Path="Department" />
<Binding Path="DataContext.JobCodes"
RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
</MultiBinding>
</ComboBox.ItemsSource>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
将多绑定转换器作为静态资源:
这是视图模型:
public class TimeSheetsViewModel
{
public ObservableCollection<Time> Times { get; set; }
public ObservableCollection<Department> Departments { get; set; }
public ObservableCollection<string> JobCodes { get; set; }
public TimeSheetsViewModel()
{
Times = new ObservableCollection<Time>();
Departments = new ObservableCollection<Department>();
GetDepartments();
JobCodes = new ObservableCollection<string>();
GetJobCodes();
}
private void GetJobCodes()
{
JobCodes = new ObservableCollection<string> { "01-A", "01-B", "02-A", "02-B", "03-A", "03-B" };
}
private void GetDepartments()
{
Departments = new ObservableCollection<Department> {
new Department("01"),
new Department("02"),
new Department("03")
};
}
}
public class Department
{
public String DepartmentCode { get; set; }
public Department(string departmentCode) { DepartmentCode = departmentCode; }
}
public class Time
{
//time in etc etc
public Department Department { get; set; }
public string Job { get; set; }
}
这会产生这个:
这可能是对现有内容的最小改动。如果你想走单独的视图模型路线,这可能是有利的(你已经有一个 "Display" 属性,它在 ViewModel 行为的领域,因为它不是数据,不应该在你的模特。
类似地,您可能希望在用户更改部门代码时采取措施,例如 clearing/nulling 职位代码(否则,他们可以设置职位代码,然后更改部门代码,并且无效配置)。逻辑越来越复杂,可能更适合 TimesViewModel。
2。您也可以使用中间属性执行此操作 您不必直接绑定到所有内容,您可以在 ViewModel 中创建一个 属性,如下所示:
public class TimesViewModel: INotifyPropertyChanged
{
//notifying property that is bound to ItemsSource in the first Combobox
public ObservableCollection<Department> Departments{ get... }
//total list of job codes
public List<string> JobCodes{ get...}
//This is the Department that's bound to SelectedItem in the first ComboBox
public Department Department
{
get
{
return department;
}
set
{
//standard notify like all your other bound properties
if (department!= value)
{
department= value;
//when this changes, our selection has changed, so update the second list's ItemsSource
DepartmentOnlyJobCodes = JobCodes.Where(jobCode => jobCode.StartsWith(Department.DepartmentCode)).ToList();
//we can also do more complex operations for example, lets clear the JobCode!
JobCode = "";
NotifyPropertyChanged("SelectedKey");
}
}
}
//an "intermediatary" Property that's bound to the second Combobox, changes with the first's selection
public ObservableCollection<string> DepartmentOnlyJobCodes{ get ... }
public string JobCode {get...}
}
这两个结果相同,您最终会将第二个组合框绑定到您以某种方式存储的列表。逻辑可能会根据您的应用程序而改变,我只是使用字典作为示例。
编辑:对编辑的回应
您可以绑定到父面板中的数据上下文,并访问子元素中的属性:
<StackPanel>
<StackPanel.DataContext>
<MultiBinding Converter="{StaticResource DeptCodeToDeptConverter}" >
<Binding Path="DepartmentCode" />
<Binding Path="DataContext.Departments" RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
</MultiBinding>
</StackPanel.DataContext>
<TextBlock Text="{Binding DepartmentCode>"/>
<TextBlock Text="{Binding DepartmentName>"/>
</StackPanel>
或者您可以添加第三个多重绑定来传递您的 属性 并使用反射 return 您需要的东西:
<TextBlock >
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DeptCodeToDeptConverter}" >
<Binding Path="DepartmentCode" />
<Binding Path="DataContext.Departments" RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
<Binding>
<Binding.Source>
<sys:String>DepartmentCode</sys:String>
</Binding.Source>
</Binding>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
然后您可以将此标记到转换器逻辑的末尾:
if (values.Length > 2 && values[2] != null)
{
return typeof(Department).GetProperty(values[2] as string).GetValue(department, null);
}
else
{
//no property string passed - assume they just want the Department object
return department;
}
我的目标是在 WPF 中拥有一组级联组合框。我正在尝试使用 MVVM 模型,但仍在学习中。
项目的一些背景信息。我正在尝试为员工编辑时间。
所以我在 DataGrid 中有一个所选员工的时间列表。 DataGrid 中的每一行都是一个时间对象。时间由一些字段 InTime、OutTime、Date、Hours 等组成。 A Time 也有 Department 和 Job。
目前我已经连接好 Department ComboBox 并正常工作,但我不确定如何根据在 Department 字段中选择的内容构建 Job 组合框。
这是我的 ViewModel 的设置方式
public ObservableCollection<Time> Times { get; set; }
public ObservableCollection<Department> Departments { get; set; }
public TimeSheetsViewModel()
{
Times = new ObservableCollection<Time>();
Departments = new ObservableCollection<Departments>();
GetDepartments();
}
private void GetDepartments()
{
/*
This section contains code to connect to my SQL Database and fills a DataTable dt
*/
if (Departments != null)
Departments.Clear();
for (int i = 0; i < dt.Rows.Count; i++)
{
Department d = new Department() { Display = dt.Rows[i]["DISPLAY"].ToString(), DepartmentCode = dt.Rows[i]["DEPARTMENT_CODE"].ToString(), CompanyCode = dt.Rows[i]["COMPANY_CODE"].ToString() };
Departments.Add(d);
}
}
这是我的 DataGrid 上的绑定
<DataGrid Grid.Row="1" Margin="15,0,15,15" Visibility="Visible" FontSize="14" HorizontalGridLinesBrush="{StaticResource Nelson2}" VerticalGridLinesBrush="{StaticResource Nelson2}" ItemsSource="{Binding Times}" SelectionMode="Single" CellEditEnding="DataGrid_CellEditEnding" RowEditEnding="DataGrid_RowEditEnding" AutoGenerateColumns="False">
<DataGridTemplateColumn Header="Department Code">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path= Department.Display}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type UserControl}}, Path=DataContext.Departments}" DisplayMemberPath="Display" SelectedValuePath="DepartmentCode" SelectedValue="{Binding Department.DepartmentCode}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid>
那么我如何实现我的工作组合框以根据为该行中的部门选择的内容填充其项目?
我假设我想将此代码放在我的同一视图模型中。
感谢任何帮助,谢谢!
编辑 (04/05/16):
如何 return 带有转换器的对象,以便我可以使用该转换器将不同的东西绑定到该对象的字段。
说这是我的转换器
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string departmentCode = values[0].ToString();
ObservableCollection<Department> Departments = values[1] as ObservableCollection<Department>;
return Departments.FirstOrDefault(Department => Department.DepartmentCode == departmentCode);
}
这是我的绑定
<TextBlock >
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DeptCodeToDeptConverter}" >
<Binding Path="DepartmentCode" />
<Binding Path="DataContext.Departments" RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
该转换器将 return 部门对象,但如果我希望 TextBlock 的文本为 Department.Name 或 Department.Location 怎么办?我是否必须为 return 每个要在不同控件中使用的字段创建一个新转换器?或者有没有办法用这个方法达到我想要的效果?
我会选择以下两种方法之一:
1.使用多绑定转换器。 您的第一个组合框的 ItemsSource 将绑定到它的事物集合。第二个可以在第一个的 SelectedItem 和第二个组合框的一些可用项目集集合上使用多绑定转换器,到 return 第二个 ItemsSource 的集合。
当第一个组合框更改它的选定项时,绑定将更新:
public class DepartmentJobComboboValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Department department = values[0] as Department;
ObservableCollection<string> jobCodes = values[1] as ObservableCollection<string>;
//do our logic to filter the job codes by department
return jobCodes.Where(jobCode => jobCode.StartsWith(department.DepartmentCode)).ToList();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
然后您可以将 SelectedItems 绑定为第一个值,将集合字典绑定为第二个值:
<DataGrid ItemsSource="{Binding Times}"
SelectionMode="Single"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Department">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path= Department.DepartmentCode}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type UserControl}}, Path=DataContext.Departments, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="DepartmentCode"
SelectedValuePath="DepartmentCode"
SelectedValue="{Binding Department.DepartmentCode}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Job code">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Job}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox SelectedValue="{Binding Job}">
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource DepartmentJobComboboValueConverter}">
<Binding Path="Department" />
<Binding Path="DataContext.JobCodes"
RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
</MultiBinding>
</ComboBox.ItemsSource>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
将多绑定转换器作为静态资源:
这是视图模型:
public class TimeSheetsViewModel
{
public ObservableCollection<Time> Times { get; set; }
public ObservableCollection<Department> Departments { get; set; }
public ObservableCollection<string> JobCodes { get; set; }
public TimeSheetsViewModel()
{
Times = new ObservableCollection<Time>();
Departments = new ObservableCollection<Department>();
GetDepartments();
JobCodes = new ObservableCollection<string>();
GetJobCodes();
}
private void GetJobCodes()
{
JobCodes = new ObservableCollection<string> { "01-A", "01-B", "02-A", "02-B", "03-A", "03-B" };
}
private void GetDepartments()
{
Departments = new ObservableCollection<Department> {
new Department("01"),
new Department("02"),
new Department("03")
};
}
}
public class Department
{
public String DepartmentCode { get; set; }
public Department(string departmentCode) { DepartmentCode = departmentCode; }
}
public class Time
{
//time in etc etc
public Department Department { get; set; }
public string Job { get; set; }
}
这会产生这个:
这可能是对现有内容的最小改动。如果你想走单独的视图模型路线,这可能是有利的(你已经有一个 "Display" 属性,它在 ViewModel 行为的领域,因为它不是数据,不应该在你的模特。
类似地,您可能希望在用户更改部门代码时采取措施,例如 clearing/nulling 职位代码(否则,他们可以设置职位代码,然后更改部门代码,并且无效配置)。逻辑越来越复杂,可能更适合 TimesViewModel。
2。您也可以使用中间属性执行此操作 您不必直接绑定到所有内容,您可以在 ViewModel 中创建一个 属性,如下所示:
public class TimesViewModel: INotifyPropertyChanged
{
//notifying property that is bound to ItemsSource in the first Combobox
public ObservableCollection<Department> Departments{ get... }
//total list of job codes
public List<string> JobCodes{ get...}
//This is the Department that's bound to SelectedItem in the first ComboBox
public Department Department
{
get
{
return department;
}
set
{
//standard notify like all your other bound properties
if (department!= value)
{
department= value;
//when this changes, our selection has changed, so update the second list's ItemsSource
DepartmentOnlyJobCodes = JobCodes.Where(jobCode => jobCode.StartsWith(Department.DepartmentCode)).ToList();
//we can also do more complex operations for example, lets clear the JobCode!
JobCode = "";
NotifyPropertyChanged("SelectedKey");
}
}
}
//an "intermediatary" Property that's bound to the second Combobox, changes with the first's selection
public ObservableCollection<string> DepartmentOnlyJobCodes{ get ... }
public string JobCode {get...}
}
这两个结果相同,您最终会将第二个组合框绑定到您以某种方式存储的列表。逻辑可能会根据您的应用程序而改变,我只是使用字典作为示例。
编辑:对编辑的回应
您可以绑定到父面板中的数据上下文,并访问子元素中的属性:
<StackPanel>
<StackPanel.DataContext>
<MultiBinding Converter="{StaticResource DeptCodeToDeptConverter}" >
<Binding Path="DepartmentCode" />
<Binding Path="DataContext.Departments" RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
</MultiBinding>
</StackPanel.DataContext>
<TextBlock Text="{Binding DepartmentCode>"/>
<TextBlock Text="{Binding DepartmentName>"/>
</StackPanel>
或者您可以添加第三个多重绑定来传递您的 属性 并使用反射 return 您需要的东西:
<TextBlock >
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DeptCodeToDeptConverter}" >
<Binding Path="DepartmentCode" />
<Binding Path="DataContext.Departments" RelativeSource="{RelativeSource Findancestor, AncestorType={x:Type UserControl}}"/>
<Binding>
<Binding.Source>
<sys:String>DepartmentCode</sys:String>
</Binding.Source>
</Binding>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
然后您可以将此标记到转换器逻辑的末尾:
if (values.Length > 2 && values[2] != null)
{
return typeof(Department).GetProperty(values[2] as string).GetValue(department, null);
}
else
{
//no property string passed - assume they just want the Department object
return department;
}