One combo box filtering another combo box in a WPF

2019-04-17 10:53发布

问题:

I have a datagrid which has two combo box columns in it. The first combo box is a list of PersonnelTypes. Depending on which PersonnelType is selected, the second combo box should fill up with a list of Resources that match the selected PersonnelType

The problem is, lets say I have two rows of data, if I change the PersonnelType of one row, the datagrid will set the itemsource for all of the Resources in every row. I only want it to filter the row that I am in, not all the rows.

Here's the xaml for the part of the datagrid that has the combo boxes:

                    <DataGridTemplateColumn  Header="Personnel Type" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <ComboBox Name="cmbPersonnelTypes" FontWeight="Bold" ItemsSource="{Binding ViewModel.PersonnelTypes, RelativeSource={RelativeSource AncestorType=Window}}"  SelectedItem="{Binding PersonnelType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="ID" DisplayMemberPath="Description" SelectionChanged="cmbPersonnelTypes_SelectionChanged" />
                                </Grid>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn  Header="Name" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <ComboBox Name="cmbPersonnelName" FontWeight="Bold" ItemsSource="{Binding ViewModel.ResourcesToChooseFrom, RelativeSource={RelativeSource AncestorType=Window},UpdateSourceTrigger=PropertyChanged}"   SelectedItem="{Binding Resource, Mode=TwoWay}" SelectedValuePath="Refno" DisplayMemberPath="Name" />
                                </Grid>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn> 

Here is the xaml for the whole data grid (just in case you need to see it):

            <DataGrid AutoGenerateColumns="False" CanUserSortColumns="False" CanUserDeleteRows="True" IsReadOnly="True" Background="LightGray" CanUserAddRows="False" Margin="5" SelectedItem="{Binding SelectedLA_JobPersonnel}" ItemsSource="{Binding LA_Personnel}" Grid.ColumnSpan="4" MouseDoubleClick="DataGrid_MouseDoubleClick_1">
                <DataGrid.Resources>
                    <ViewModels:BindingProxy x:Key="proxy" Data="{Binding}" />
                </DataGrid.Resources>
                <DataGrid.Columns>
                    <DataGridTemplateColumn  Header="Personnel Type" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <ComboBox Name="cmbPersonnelTypes" FontWeight="Bold" ItemsSource="{Binding ViewModel.PersonnelTypes, RelativeSource={RelativeSource AncestorType=Window}}"  SelectedItem="{Binding PersonnelType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="ID" DisplayMemberPath="Description" SelectionChanged="cmbPersonnelTypes_SelectionChanged" />
                                </Grid>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn  Header="Name" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <ComboBox Name="cmbPersonnelName" FontWeight="Bold" ItemsSource="{Binding ViewModel.ResourcesToChooseFrom, RelativeSource={RelativeSource AncestorType=Window},UpdateSourceTrigger=PropertyChanged}"   SelectedItem="{Binding Resource, Mode=TwoWay}" SelectedValuePath="Refno" DisplayMemberPath="Name" />
                                </Grid>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>                            <DataGridTemplateColumn Header="Date Out"  Width="20*" >

                        <DataGridTemplateColumn.CellTemplate>

                            <DataTemplate>

                                <TextBlock Background="LightGray" FontWeight="Bold" Text="{Binding DateOut, Mode=TwoWay, Converter={StaticResource thisNullDateConverter}, StringFormat={}{0:MMM-dd-yyyy hh:ss tt}}">
                                </TextBlock>
                            </DataTemplate>

                        </DataGridTemplateColumn.CellTemplate>

                        <DataGridTemplateColumn.CellEditingTemplate>

                            <DataTemplate>
                                <Toolkit:DateTimePicker Background="LightGray" FontWeight="Bold" Value="{Binding Path=DateOut, Mode=TwoWay, Converter={StaticResource thisNullDateConverter}}" Format="Custom" FormatString="MMM dd yyyy hh:ss tt"></Toolkit:DateTimePicker>

                            </DataTemplate>

                        </DataGridTemplateColumn.CellEditingTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Header="Date In"  Width="20*">

                        <DataGridTemplateColumn.CellTemplate>

                            <DataTemplate>

                                <TextBlock Background="LightGray" FontWeight="Bold" Text="{Binding DateIn, Mode=TwoWay, Converter={StaticResource thisNullDateConverter}, StringFormat={}{0:MMM-dd-yyyy hh:ss tt}}">
                                </TextBlock>
                            </DataTemplate>

                        </DataGridTemplateColumn.CellTemplate>

                        <DataGridTemplateColumn.CellEditingTemplate>

                            <DataTemplate>
                                <Toolkit:DateTimePicker Background="LightGray" FontWeight="Bold" Value="{Binding Path=DateIn, Mode=TwoWay, Converter={StaticResource thisNullDateConverter}}" Format="Custom" FormatString="MMM dd yyyy hh:ss tt"></Toolkit:DateTimePicker>

                            </DataTemplate>

                        </DataGridTemplateColumn.CellEditingTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>


            </DataGrid>

Here is the code behind for the xaml (xaml.cs):

    public JobEditorViewModel ViewModel
    {
        get { return viewModel; }
    }

private void cmbPersonnelTypes_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var combobox = sender as ComboBox;
        if (combobox != null)
        {
            var selectedPersonnelType = combobox.SelectedItem as PersonnelType;
            viewModel.SetResourcesToChooseFrom(selectedPersonnelType);
        }
    }

Here is the code in the viewModel:

public BindingList<PersonnelType> PersonnelTypes
{
    get; set;
}
public JobEditorViewModel(int jobid, string region, DataAccessDataContext db, ServiceUserControlViewModel serviceViewModel)
{

    PersonnelTypes = new BindingList<PersonnelType>(_db.PersonnelTypes.OrderBy(p => p.Head).ThenBy(p => p.Description).ToList());

}


private BindingList<Resource> _resourcesToChooseFrom;


public BindingList<Resource> ResourcesToChooseFrom
{
    get { return _resourcesToChooseFrom; }
    set
    {
        _resourcesToChooseFrom = value;
        NotifyPropertyChanged("ResourcesToChooseFrom");
    }
}

public void SetResourcesToChooseFrom(PersonnelType personnelType)
{
   ResourcesToChooseFrom =
        new BindingList<Resource>(_db.Resources.Where(r => r.Head == personnelType.Head && r.Refno > 2).OrderBy(r=>r.Name).ToList());
}

If you need to see more, let me know

回答1:

Well, with some help from a colleague here at work, we figured out what I needed to do. Multibinding is the answer. First off we kind of hacked around so that the two combo boxes could be in the same column by placing them both in a grid and placing the grid in the one column. So now both combo boxes can see each other because they are in the same DataGridTemplateColumn. Before, we couldn't get them to see each other because they lost scope of each other in being two separate DataGridTemplateColumns.

Here's what we did in the xaml:

                                <DataGridTemplateColumn  Header="Personnel Type-Name" Width="Auto" >
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <Grid >
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="170"/>
                                                    <ColumnDefinition Width="*"/>
                                                </Grid.ColumnDefinitions>

                                                <Border Grid.Column="0" BorderBrush="Black" BorderThickness="1">
                                                    <TextBlock Text="{Binding PersonnelType.Description}"/>
                                                </Border>
                                                <Border Grid.Column="1" BorderBrush="Black" BorderThickness="1">
                                                    <TextBlock  Text="{Binding Resource.Name}"/>
                                                </Border>

                                            </Grid>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>


                                    <DataGridTemplateColumn.CellEditingTemplate>
                                        <DataTemplate>
                                            <Grid >
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="170"/>
                                                    <ColumnDefinition Width="*"/>
                                                </Grid.ColumnDefinitions>


                                                <ComboBox Name="cmbPersonnelTypes" Grid.Column="0" FontWeight="Bold" ItemsSource="{Binding ViewModel.PersonnelTypes, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding PersonnelType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="ID" DisplayMemberPath="Description" />
                                                <ComboBox Name="cmbPersonnelName" Grid.Column="1"  FontWeight="Bold" SelectedItem="{Binding Resource, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="Refno" DisplayMemberPath="Name" >
                                                    <ComboBox.ItemsSource>
                                                        <MultiBinding Converter="{StaticResource FilteredPersonnelConverter}">
                                                            <Binding Path="ViewModel.AvailablePersonnel" RelativeSource="{RelativeSource AncestorType=Window}"/>
                                                            <Binding Path="SelectedItem" ElementName="cmbPersonnelTypes"/>
                                                            <Binding Path="ViewModel.SelectedGlobalResourceViewOption" RelativeSource="{RelativeSource AncestorType=Window}"/>
                                                        </MultiBinding>
                                                    </ComboBox.ItemsSource>
                                                </ComboBox>

                                            </Grid>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellEditingTemplate>

                                </DataGridTemplateColumn>

You'll notice there is a ValueConverter in the MultiBinding called FilteredPersonnelConverter. This value converter does all the filtering for me. Here's the code for that:

public class FilteredPersonnelListValueConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var allResources = values[0] as IList<Resource>;
        var personnelType = values[1] as PersonnelType;
        var selectedGlobalResourceView = values[2] as ResourceViewOption;
        if (personnelType == null)
            return allResources;
        if(selectedGlobalResourceView.ResourceViewTitle=="Regional")
            return allResources.Where(r => r.Head == personnelType.Head && r.Obsolete == false && r.Location.Region.RegionID.Trim()==SettingsManager.OpsMgrSettings.Region.Trim()).OrderBy(r => r.Name).ToList();
        if (selectedGlobalResourceView.ResourceViewTitle == "Local")
            return allResources.Where(r => r.Head == personnelType.Head && r.Obsolete == false && r.LocnID.Trim() == SettingsManager.OpsMgrSettings.LOCNCODE.Trim()).OrderBy(r => r.Name).ToList();

        return allResources.Where(r => r.Head == personnelType.Head &&r.Obsolete==false).OrderBy(r => r.Name).ToList();

    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

So if anyone else is doing something like this, look into Multibinding, it will change your life



回答2:

When the user changes the PersonelType dropdown in the view, the ViewModel should then filter the list of Resources (which should update the second dropdown box with the correct information).

Really your view model just needs to be set up to respond to changes on the view. That's what it boils down to.

Looking here:

public JobEditorViewModel(int jobid, string region, DataAccessDataContext db, ServiceUserControlViewModel serviceViewModel)
{

    PersonnelTypes = new BindingList<PersonnelType>(_db.PersonnelTypes.OrderBy(p => p.Head).ThenBy(p => p.Description).ToList());

}

it looks like you set the personel types in the constructor, but you aren't responding to user changes. You need to do that in the PersonelType Binding in your view model.

EDIT

I see now that you're handling the selection changed. But really I think this should be done in the view model itself. Specifically because you actually access the viewmodel from the view to do make your changes.

Example

So here's my VM:

class ViewModel : INotifyPropertyChanged
{
    DispatcherTimer timer = new DispatcherTimer();

    public ViewModel()
    {
        // Create my observable collection
        this.DateTimes = new ObservableCollection<DateTime>();

        // Every second add anothe ritem
        timer.Interval = TimeSpan.FromSeconds(1);
        timer.Tick += new EventHandler(timer_Tick);
        timer.Start();
    }

    void timer_Tick(object sender, EventArgs e)
    {
        // This adds to the collection
        this.DateTimes.Add(DateTime.Now);
    }

    public ObservableCollection<DateTime> DateTimes { get; private set; }

    public event PropertyChangedEventHandler PropertyChanged;
}

and My View:

<ListBox ItemsSource="{Binding DateTimes}"/> 

Notice that I don't rebuild the collection. I just set it once, and then change it's size as neccesary.