Saturday, March 25, 2017

WPF/MVVM viewing and editing data in a many-to-many relationship (Part 2 of 2)

This is a continuation of a previous post where I explained my scenario and how I displayed data in a many-to-many relationship.  Now I'll explain how I ended up editing and saving.  It seems a bit more complicated than it should be, but it works.

Editing

I have two objects named Racers and Divisions. One racer can be in zero or more divisions and one division can contain zero or more racers.  I'm editing the relationship on a Racer maintenance form where a racer's divisions are selected in a checkbox list.  See the previous post for details.

View Model

To accommodate this I have two commands in my Maintain Racer view model, one for adding a division and one for removing a division.  And a keep the original list of racer divisions in case the user cancels.  Here are the relevant parts of the view model: 
        private List<Division> _originalRacerDivisions;

        public ICommand AddDivisionCommand { get; set; }
        public ICommand RemoveDivisionCommand { get; set; }

        // Constructor
        public RacerDetailViewModel(IDerbyDataService derbyDataService)
        {
            ... snip ...
            AddDivisionCommand = new CustomCommand(AddDivision, CanAddRemoveDivision);
            RemoveDivisionCommand = new CustomCommand(RemoveDivision, CanAddRemoveDivision);
            ... snip ...
        }

        private void LoadRacer(Racer racer)
        {
            ... snip ...
            _originalRacerDivisions = racer.Divisions;
            ... snip ...
        }
 
        private void AddDivision(object obj)
        {
            if (!(obj is Division))
                throw new ArgumentException("Invalid division");

            var divisionToAdd = (Division)obj;
            var racerDivisions = RacerDivisions;

            racerDivisions.Add(divisionToAdd);
            RacerDivisions = racerDivisions;
        }

        private void RemoveDivision(object obj)
        {
            if (!(obj is Division))
                throw new ArgumentException("Invalid division");

            var divisionToRemove = (Division)obj;
            var racerDivisions = RacerDivisions;

            racerDivisions.Remove(divisionToRemove);
            RacerDivisions = racerDivisions;
        }

        private bool CanAddRemoveDivision(object obj)
        {
            return true;
        }
    }
}


View

In the view I have a template column with a checkbox.  I used this instead of a checkbox column because a checkbox column requires two clicks- one to select the row and a second to change the value.  This way only one click is needed.

There are two triggers- one handles the Checked event and calls the AddDivision command, and the other handles the Unchecked event and called RemoveDivision command.
<DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid>
                                <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center" Margin="5">
                                    <CheckBox.IsChecked>
                                        <MultiBinding Converter="{StaticResource localDivisionToBooleanConverter}" 
                                                      Mode="OneWay">
                                            <Binding Path="."></Binding>
                                            <Binding Path="DataContext.RacerDivisions" 
                                                     RelativeSource="{RelativeSource AncestorType=Window}"></Binding>
                                        </MultiBinding>
                                    </CheckBox.IsChecked>
                                    <i:Interaction.Triggers>
                                        <i:EventTrigger EventName="Checked">
                                            <i:InvokeCommandAction Command="{Binding DataContext.AddDivisionCommand, RelativeSource={RelativeSource AncestorType=Window}}" 
                                                                   CommandParameter="{Binding Path=.}" />
                                        </i:EventTrigger>
                                        <i:EventTrigger EventName="Unchecked">
                                            <i:InvokeCommandAction Command="{Binding DataContext.RemoveDivisionCommand, RelativeSource={RelativeSource AncestorType=Window}}" 
                                                                   CommandParameter="{Binding Path=.}" />
                                        </i:EventTrigger>
                                    </i:Interaction.Triggers>
                                </CheckBox>
                            </Grid>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>

Cancelling

If the user decides to cancel, the racer's divisions need to be set back to their original values:
private void CancelRacer(object obj)
        {
            racer.Divisions = _originalRacerDivisions;
            ... snip ...
        }

No comments:

Post a Comment