Thursday, March 16, 2017

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

In the WPF/MVVM world some things seem to be a lot more complicated than they should be.  One of those things is presenting and editing data in a many-to-many relationship.  Once again, thanks to Google for leading to this article that was the starting point for of my solution- https://social.technet.microsoft.com/wiki/contents/articles/20719.wpf-displaying-and-editing-many-to-many-relational-data-in-a-datagrid.aspx

My Scenario

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.  The data is edited on a Racer maintenance form that looks like this:
The corresponding database tables look like this:


These are my model classes.  Note that there are just two classes- Racer and Division.  The navigation properties (Racer.Divisions and Divisions.Racers) are the link between them and are equivalent to the RacerDivisions link table in the database.
  public class Racer : ModelBase, IModificationHistory  
   {  
     public Racer()  
     {  
       Divisions = new List<Division>();   
     }  
     private int racerId;  
     private int carNumber;  
     private string carName;  
     private string ownerLastName;  
     private string ownerFirstName;  
     private List<Division> divisions;   
     private DateTime dateModified;  
     private DateTime dateCreated;  

     public int RacerId  
     {  
       get { return racerId; }  
       set  
       {  
         if (value == racerId) return;  
         racerId = value;  
         RaisePropertyChanged("RacerId");  
       }  
     }  

     // The other getters/setters here
   }

   public class Division : ModelBase, IModificationHistory
    {
        public Division()
        {
            Racers = new List();
        }

        private int divisionId;
        private int sequence;
        private string name;
        private string logoPath;
        private bool includeInChampionship;
        private bool isChampionship;
        private List racers;
        private DateTime dateModified;
        private DateTime dateCreated;

        public int DivisionId
        {
            get { return divisionId; }
            set
            {
                divisionId = value;
                RaisePropertyChanged("DivisionId");
            }
        }

        // The other getters/setters here
    }


Showing the Data

Like the TechNet article, I am showing all the Divisions in a DataGrid with a check box column representing the divisions the racer is a member of (ie. is linked to).  So in my viewmodel I expose a list of all Divisions and a list of the Racer's Divisions (from the Racer object's graph- Racer.Divisions).  Here are the relevant viewmodel snippets:
   public class RacerDetailViewModel : ViewModelBase, IDataErrorInfo  
   {  
     private List<Division> _divisions;      
     private Racer _selectedRacer { get; set; }   
     
     // Various properties exposing Racer data to the view
     
     // The list of all Divisions
     public List<Division> Divisions  
     {  
       get { return _divisions; }  
     }  

     // The Racer Instance's Divisions 
     public ObservableCollection<Division> RacerDivisions  
     {  
       get { return _selectedRacer.Divisions.ToObservableCollection(); }  
       set  
       {  
         _selectedRacer.Divisions = value.ToList();  
         SetRacerDirty();  
         RaisePropertyChanged("Divisions");  
       }  
     }  

     // Other unrelated parts of the viewmodel (constructor, command handlers, etc.)     
 
   }  

The checkboxes need a boolean value to bind to.  I have a converter for this that takes two parameters: a Division object and a list of Racer.Divisions.  If Racer.Divisions contains the Division, it returns True, and if not- False.
  class DivisionToBooleanConverter : IMultiValueConverter  
   {  
     public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)  
     {  
       if (!(values[0] is Division))  
         throw new ArgumentException("DivisionToBooleanConverter- First arg must be Division");  
       if (!(values[1] is ObservableCollection<Division>))  
         throw new ArgumentException("DivisionToBooleanConverter- Second arg must be RacerDivisions.");  
       var division = (Division)values[0];  
       var racerDivisions = (ObservableCollection<Division>)values[1];  
       return racerDivisions.Contains(division);  
     }  
     public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)  
     {  
       throw new NotImplementedException();  
     }  
   }  

And finally, the WPF DataGrid that shows the data.  It is bound to the list of all Divisions.  I put the checkboxes in a template column because in a regular checkbox column the checkbox has to be clicked twice to change its value.  The current division and list of Racer's divisions are passed to the converter to populate the row's checkbox.
 <DataGrid Grid.Row="4" Grid.Column="1" Margin="5" ItemsSource="{Binding Divisions}"   
          AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="None">  
       <DataGrid.Columns>  
         <DataGridTextColumn Binding="{Binding Name}"></DataGridTextColumn>  
         <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>  
                 </CheckBox>  
               </Grid>  
             </DataTemplate>  
           </DataGridTemplateColumn.CellTemplate>  
         </DataGridTemplateColumn>  
       </DataGrid.Columns>  
     </DataGrid>  

And there you have it!  Next I'll explain how the data is edited and saved.  That will be in a new post because this one is long enough.

No comments:

Post a Comment