4

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?

CR_eeper
  • 65
  • 8

1 Answers1

4

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:

enter image description here

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;
}
Joe
  • 6,773
  • 2
  • 47
  • 81
  • Thanks @Joe, I'm a bit confused here though. Pretty new at this stuff. So does each row in my DataGrid need to be bound to it's own instance of a view model? – CR_eeper Apr 04 '16 at 22:04
  • Sorry, should have clarified, yes, each row would have a view model. That's how I'd go about it. If it were a simple table of data, I don't think it warrants it, but you've clearly got more complex functionality in there. You could use probably use multi binding converters without row view models though. How many rows are you expecting? – Joe Apr 04 '16 at 23:15
  • Looking at how you've implemented it, multI binding should work fine without Time view models. I'll knock up a more detailed example tomorrow (9 hours or so). What's defining which collection gos into the second combo box (using the department code from the first I presume)? – Joe Apr 04 '16 at 23:22
  • Okay, I'm not opposed to a view model for each line, but with my experience I didn't even know that was a possibility. I would expect about 5-7 rows (one for each day they work in a week), but it could be upwards for 20-30 (unlikely). Yeah the Department code is actually appended on to the start of Job Codes for example; 003 is my department code Job codes could be 003-00001 to 003-00025. So using the department code is how I make the list of Jobs for that department. Thanks for your help! I'll check back tomorrow sometime – CR_eeper Apr 04 '16 at 23:51
  • @CR_eeper Major edit showing how to do multibinding in more detail. – Joe Apr 05 '16 at 10:23
  • Also made the TimeViewModel more detailed – Joe Apr 05 '16 at 10:29
  • Awesome! Works like a charm! You've helped out big time here, thanks – CR_eeper Apr 05 '16 at 20:12
  • One more thing @Joe say I want to have a converter that returns a Department Object is there a way to bind to the Departments different fields like Department.Location or Department.Name with out having to make a new converter that returns each of the fields I want? See the edit for more details. No rush cause this isn't that important, just wondering. – CR_eeper Apr 05 '16 at 23:16
  • If the fields are in a common control like, say a stack panel you could bind the Department (using the fancy converter) to it's data context and have the sub-components just use the standard Path binding. (You could even just have a container with a single component in). Alternatively, if you are doing it a lot singularly (so not a bunch of department . somethings in the same layout) you could add a third multibinding field "Property" and use a bit of reflection to return the property you want in the converter logic. – Joe Apr 06 '16 at 09:08
  • Thanks! You seem to be very knowledgeable in this, it has helped a lot – CR_eeper Apr 06 '16 at 19:09