简体   繁体   中英

Cascading ComboBox in DataGrid with MVVM

My goal is to have a set of cascading comboboxes in WPF. I'm trying to use the MVVM model, but still learning.

Some background information on the project. I'm trying to edit times for employees.

So I have a list of Times for the selected employee in a DataGrid. Each row in the DataGrid is a Time object. A Time consists of some fields InTime, OutTime, Date, Hours... ect. A Time also has a Department and a Job.

Currently I have the Department ComboBox wired up and working, but I'm not sure how to build the Job combobox based on what is selected in the Department field.

Here is how my ViewModel is set up

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

This is the binding on my 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>

So how do I implement my job combobox to populate its items based on whatever is selected for department in that row?

I'm assuming I want to put the code for this in my same view model.

Any help is appreciated, Thanks!

EDIT (04/05/16):

How can I return an Object with a Converter so that I can use that Converter to bind different things to fields of that Object.

Say this is my converter

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

And this is my binding

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

That converter will return a Department Object, but what if I want the TextBlock's Text to be Department.Name or Department.Location. Do I have to create a new converter to return each of the field I want to use in different controls? Or is there a way to achieve what I want using this method?

I'd choose one of two ways to do this:

1. Use a multi binding converter. Your first combobox' ItemsSource will be bound to it's collection of things. The second's could use a multibinding converter on the first's SelectedItem and some collection of available item sets for the second combobox, to return the collection for the second's ItemsSource.

When the first combobox changes it's selected item, the binding will update:

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

You can then bind the SelectedItems as the first Value, and the dictionary of collections as your second value:

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

With the multi binding converter as a static resource:

Here's the view model:

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

This produces this:

在此处输入图片说明

This is probably the least change to what you have already. If you want to go the separate view model route, which may be advantageous (you already have a "Display" property, which is in the realms of ViewModel behavior, as it's not data and shouldn't be in your Model.

Similarly, you might want to take actions when the user changes the department code, such as clearing/nulling the job code (otherwise, they can set the job code, then change the department code and have an invalid configuration). The logic is getting complex and would probably fit more nicely in a TimesViewModel.

2. You can also do this using intermediate properties You don't have to bind to everything directly, you could create a property in your ViewModel like this:

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

These both have the same result, you will ultimately bind your second ComboBoxes to a List you've stored somehow. The logic can change depending on your application, I've just used a dictionary for an example.

Edit: response to edit

You could bind to a data context in a parent panel, and access the properties in the child elements:

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

Or you could add a third multibinding to pass your Property and use reflection to return what you need:

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

You can then tag on this to the end of your converter logic:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM