Observable Collections, Data Binding, And Complex Automation In WPF

A Tale Of Three Widgets

 
For this program, I created three custom widgets, each with some sort of measurement unit attached to it as well as other properties and methods. The first Widget has three Lengths as properties and the user needs the units to be in inches. Well easy enough, slap a text tag "Inch(es)" after the values the user enters and done. Well, not really. What if the user needs to see what the values are in Metric or what if the person entering the values only has the values in feet, and is very bad at multiplication...
 
Now, we have a challenge - how to assign a unit of measurement to something that will automatically update when the user switches measurement units, like inches to feet; or even toggling between English (US) and Metric (SI) measurement systems.
 
Observable Collections, Data Binding, And Complex Automation In WPF
 
In the following screenshot, we have updated Widget One's State Of Mind and Color which are both reflected in the Widget One TreeView Item. The Length units have also been changed from the default inches - "in (US)" to feet - "ft (US)".
 
Observable Collections, Data Binding, And Complex Automation In WPF
 
Length will be the first measurement unit to incorporate into our application. For Length, inches, feet - for English units; centimeters, millimeters, and meters will be supported for Metric units. We need an object, a container for our units. We will call this container a Bucket. The Bucket will hold any type of measurement that we need, it will become our class to deal with the Measurement Systems. Looking forward we also want to support Area with square inches, centimeters, and millimeters as values; and Volume with gallons, liters, cubic - feet, meters, centimeters, and millimeters.
 
In order to support changing units, we need to maintain a "control" value. The control value will be stored in the default units - for Length that would be inches. Using the control value, a display value of any unit is created and maintained. We will call the control value the "RealBucket" and the value the user sees in the selected units is the "DisplayBucket". So we know we need two values for Length - LengthRealBucket and LengthDisplayBucket, same for Volume and Area - VolumeRealBucket, VolumeDisplayBucket, AreaRealBucket, and finally AreaDisplayBucket.
 
There is a relationship between the units of measurement. For example, the default Length unit will be inches; from this, we know that there are .08333 inches per foot, 2.54 centimeters per inch, 25.4 millimeters per inch and so on. From this, we can define our Bucket as a formulaic relation to one another. The Bucket will have the following properties to encapsulate a measurement unit,
  • Units it represents - Length, Area, Volume
  • Measurement system of the Bucket - (US or SI)
  • Conversion value - numeric conversion from the default unit - (Inches = 1.00, Feet = .08333)
  • Shift value if needed that will through addition or subtraction will adjust the overall quantity
An example of a measurement unit that would need the shift would be Temperature. Degrees Celcius would be defined as a new Bucket ("Celcius", "Degrees Celcius", 1.0, 0.0, BucketType.Temperature) and Fahrenheit relation to Celcius is Temp(celsius) * 1.8 + 32. So the formula would be as follows: new Bucket ("Fahrenheit", "Degrees Fahrenheit", 1.8, 32.0, BucketType.Temperature).
 
So our measurement units class is taking shape. Below is the definition for Length Units. Area and Volume are very similar, they can be viewed in the source. Note however; that any type and any number of measurement types can be added - such as Mass, Velocity, Temperature, etc...
  1. // Define Length  
  2. LengthBuckets = new[]  
  3. {  
  4.   new Bucket("in""US", 1.0, 0.0, BucketType.Length),  
  5.   new Bucket("ft""US", 0.0833333, 0.0, BucketType.Length),  
  6.   new Bucket("cm""SI", 2.54, 0.0, BucketType.Length),  
  7.   new Bucket("mm""SI", 25.4, 0.0, BucketType.Length),  
  8.   new Bucket("m""SI", 0.0254, 0.0, BucketType.Length)  
  9. };  
  10. // Set to default  
  11. LengthDisplayBucket = LengthBuckets[0];  
  12. LengthRealBucket = LengthBuckets[0];  
Note the Bucket Type, since we have Area, Length, and Volume we need to k now which bucket we are dealing with. Definition of our Buckets is only one step, now we have to convert these when the user changes them so we need an event mechanism - the Property Changed Event Handler. Each unit property will have a name associated with it, for Length we assigned the property names of "LengthRealBucket" and "LengthDisplayBucket". The name of the property gets pushed via the property changed event,
  1. /// <summary>  
  2. ///  Event handler to notify when a property has changed and passes the changed property up the stack  
  3. /// </summary>  
  4. public event PropertyChangedEventHandler PropertyChanged;  
  5. internal void Changed(string id)  
  6. {  
  7.      PropertyChangedEventHandler eventhandler = PropertyChanged;  
  8.      if (null != eventhandler)   
  9.         eventhandler(thisnew PropertyChangedEventArgs(id));  
  10. }  
Using the Bucket definition we can create a calculate routine that will handle conversion between compatible types of units. In other words, the routine can switch between all length units, all volume units, and all area units by using the conversion values we associated with each Bucket.
  1. internal static double   
  2. Calculate(Bucket bucket_before, Bucket bucket_after, double value)  
  3. {  
  4.      if (bucket_before.BType != bucket_after.BType) throw   
  5.         new InvalidOperationException("Conversion Between Incorrect Types!");  
  6.      if ((bucket_before.BucketShift == bucket_after.BucketShift) &&   
  7.             (bucket_before.BucketScale == bucket_after.BucketScale)) return value;  
  8.      double retVal = (value - bucket_before.BucketShift) / bucket_before.BucketScale;  
  9.      return (retVal * bucket_after.BucketScale + bucket_after.BucketShift);  
  10. }  
Two routines are needed that will call the Calculate method. The first is to calculate the display bucket from the real bucket as shown here. Note that the other units of Area and Volume have been included and are controlled by the Bucket Type. Don't worry about copying these code fragments, just follow along. I'll make the code for the complete classes available at the end of the article. I can't give out the whole source since I'm using the Property Grid from the Xceed Library. It's a license issue.
  1. internal double   
  2. CalcFromRealBucket(BucketType btype, double value)   
  3. {  
  4.     switch(btype)  
  5.     {  
  6.         case BucketType.Area:  
  7.             return Calculate(AreaRealBucket, AreaDisplayBucket, value);  
  8.         case BucketType.Length:  
  9.             return Calculate(LengthRealBucket, LengthDisplayBucket, value);  
  10.         case BucketType.Volume:  
  11.             return Calculate(VolumeRealBucket, VolumeDisplayBucket, value);  
  12.         default:  
  13.             throw new   
  14.             InvalidOperationException("Error!  Bucket Types Supported Are Area, Length, And Volume");  
  15.     }  // End switch  
  16. // End CalcFromRealBucket()  
A quick tidbit on WPF before we look at the actual Widget that will use the Units class. As you can see from the screen shot WPF is really great for making the UI look like something more than the plain old app. Even applying a linear gradient to the combo boxes makes them look like something special, something different from the same old plain combo box. Yeah, it is great, until the user puts the focus on the control - then YUK!!
 
If you do any customization of the GUI, then be prepared for having to deal with GotFocus / LostFocus events to force things to look pretty. In the case of the button and the window background I used a LinearGradient to define the color scheme. I won't go into how to use a LinearGradient since there is a lot on the web about it. The GotFocus event is used to override the highlight colors by defining a new foreground and background. We also capture the LinearGradient of the current background before switching. Define a public variable to hold the LinearGradient background. Then in the GotFocus event we save the LinearGradient background before setting it to Gold with a foreground of black for visibility.
  1. // Combo box Linear Gradient variable to store the current Background  
  2. public static LinearGradientBrush lgb_ComboBackground;  
  3. .  
  4. .  
  5. .  
  6. /// <summary>  
  7. /// Next two routines handle the glamour for the Bucket System Combo Box  
  8. /// </summary>  
  9. /// <param name="sender"></param>  
  10. /// <param name="e"></param>  
  11. private void   
  12. cmbBucketSystem_GotFocus(object sender, RoutedEventArgs e)  
  13. {  
  14.     // Store the current glamour to make it easy to restore at lostfocus()  
  15.     lgb_ComboBackground = (LinearGradientBrush) cmbBucketSystem.Background;       
  16.     cmbBucketSystem.Background = Brushes.Gold;  
  17.     cmbBucketSystem.Foreground = Brushes.Black;  
  18. }  
  19. private void cmbBucketSystem_LostFocus  
  20.     (object sender, RoutedEventArgs e)  
  21. {  
  22.     cmbBucketSystem.Foreground = Brushes.Gold;  
  23.     cmbBucketSystem.Background = lgb_ComboBackground;  
  24. }  
The Measurement Unit class Buckets is done, now it needs to be utilized. A custom Widget will be created that will need Length measurements. The Widget will contain three lengths associated with it, why three - just decided at random to have three different lengths. Each will update when the units or the measurement system changes. Below are the protected property variables that Widget One will have. The Properties are Name, Color, State Of Mind, Length A, Length B, Length C, and Date Created.
 
There are two other Widgets, one has Area as a measurement unit and the other uses Volume. The set up for each is very similar to Widget One, but I included them in the code to show some more complexity and versatility as well.
  1. // Declare the vars needed - These are the properties of Widget One  
  2. // Widget Name  
  3. private string _widget_name = string.Empty;            
  4. // Widget Color  
  5. private string _widget_color = string.Empty;           
  6. // Widget State of mind... ;)  
  7. private string _widget_state = string.Empty;           
  8. // Store the date and time the widget was created  
  9. private DateTime _widget_created = DateTime.Now;       
  10. // Length A  
  11. private double _widget_length_A = 0.0;                 
  12. // Length B  
  13. private double _widget_length_B = 0.0;                 
  14. // Length C  
  15. private double _widget_length_C = 0.0;                 
  16. // Stores the current unit of measurement  
  17. private string _widget_length_bucket = string.Empty;   
For each length the Widget will need to maintain two values as discussed earlier.
  1. // Length A of Widget One - Control value  
  2. public double RealLengthA  
  3. {  
  4.     get { return _widget_length_A; }  
  5.     set  
  6.     {  
  7.         if (value != _widget_length_A)  
  8.         {  
  9.             _widget_length_A = value;  
  10.             PropChanged("RealLengthA");  
  11.             PropChanged("DisplayLengthA");  
  12.         }  
  13.     }  
  14. }  
  15.   
  16. // Widget Length A  
  17. public double DisplayLengthA  
  18. {  
  19.     get { return   
  20.         Buckets.Instance.CalcFromRealBucket(BucketType.Length, _widget_length_A); }  
  21.     set  
  22.     {  
  23.         double len_val = Buckets.Instance.CalcToRealBucket(BucketType.Length, value);  
  24.         if (len_val != _widget_length_A)  
  25.         {  
  26.             _widget_length_A = len_val;  
  27.             PropChanged("RealLengthA");  
  28.             PropChanged("DisplayLengthA");  
  29.         }  
  30.      }  
  31. }  
For each, real and display; if the value changes we want both to be in sync so we raise the Property Changed on both values: "RealLengthA" and "DisplayLengthA". We need to attach the PropertyChanged event to the Widget as well as the routine that will handle the event. The Buckets class will take care of knowing how to update the values via routines just mentioned Buckets.Instance.CalcFromRealBucket and Buckets.Instance.CalcFromDisplayBucket.
  1. /// <summary>  
  2. /// Widget One definition  
  3. /// </summary>  
  4. public class widgetOne  
  5. {  
  6.     public widgetOne()  
  7.     {  
  8.         Buckets.Instance.PropertyChanged += BucketsChanged;  
  9.         EnumBinding = SystemType.UnitedStates;  
  10.     }  
  11.     void BucketsChanged(object sender, PropertyChangedEventArgs e)  
  12.     {  
  13.         PropChanged("RealLengthA");  
  14.         PropChanged("DisplayLengthA");  
  15.         PropChanged("RealLengthB");  
  16.         PropChanged("DisplayLengthB");  
  17.         PropChanged("RealLengthC");  
  18.         PropChanged("DisplayLengthC");  
  19.         PropChanged("LengthBuckets");  
  20.     }  
  21. .  
  22. .  
  23. .  
Now the code for PropChanged which declares the Property Changed Event Handler and passes the changed Property Name to the handler. The other portion needed for this mechanism is the Enumeration Binding which assigns the correct units when the system of measurement changes from English to Metric and vice versa. It will call the support routine TypeChanged to assign the correct default value.
  1. /// <summary>  
  2. /// Controls switching between measurement systems  
  3. /// </summary>  
  4. private SystemType _enumBinding;  
  5.   
  6. public SystemType EnumBinding  
  7. {  
  8.     get  
  9.     {  
  10.         return _enumBinding;  
  11.     }  
  12.     set  
  13.     {  
  14.         if (_enumBinding != value)  
  15.         {  
  16.             if (value == SystemType.UnitedStates)  
  17.             {  
  18.                 _enumBinding = SystemType.UnitedStates;  
  19.                 TypeChanged();  
  20.             }  
  21.             else  
  22.             {  
  23.                 _enumBinding = SystemType.InternationalSystem;  
  24.                 TypeChanged();  
  25.             }  
  26.             if (PropertyChanged != null)  
  27.                     PropertyChanged(thisnew   
  28.                     PropertyChangedEventArgs("EnumBinding"));  
  29.         }  
  30.         else  
  31.         {   
  32.             if (value == SystemType.UnitedStates)  
  33.             {  
  34.                 //_enumBinding = SystemType.InternationalSystem;  
  35.                 _enumBinding = SystemType.UnitedStates;  
  36.                 TypeChanged();  
  37.             }  
  38.             else  
  39.             {  
  40.                 //_enumBinding = SystemType.UnitedStates;  
  41.                 _enumBinding = SystemType.InternationalSystem;  
  42.                 TypeChanged();  
  43.             }  
  44.             if (PropertyChanged != null)  
  45.                 PropertyChanged(thisnew   
  46.                 PropertyChangedEventArgs("EnumBinding"));  
  47.   
  48.         }  
  49.     }  
  50. }  // End EnumBinding  
  51.   
  52. /// <summary>  
  53. /// Handles switching between measurement systems United States and International  
  54. /// </summary>  
  55. public void TypeChanged()  
  56. {  
  57.     // United States System Of Measurement  
  58.     if (EnumBinding.Equals(SystemType.UnitedStates))          
  59.     {  
  60.         // Set to Inches  
  61.         Buckets.Instance.LengthDisplayBucket = Buckets.Instance.LengthBuckets[0];    
  62.     }  
  63.     // International System Of Measurement  
  64.     else       
  65.     {  
  66.         // Set to Centimeters  
  67.         Buckets.Instance.LengthDisplayBucket = Buckets.Instance.LengthBuckets[2];    
  68.     }  
  69. }  
Now that we have all the code we need for Widget One, be sure to add using System.Collections.ObjectModel so that we can use the ObservableCollection to define our list of Widget Ones(s).
  1. /// <summary>  
  2. /// Define the Observable Collection for Widget One  
  3. /// </summary>  
  4. public class widgetOneList : ObservableCollection<widgetOne>  
  5. {  
  6.     public widgetOneList()  
  7.         : base()  
  8.     { }  
  9. }  
In the main program we can utilize our new Widget.
  1. // Observable collection list for all Widget One creations  
  2. public static widgetOneList list_WidgetOne = new widgetOneList();     
The following routine creates a new Widget and adds it to the TreeView. In the XAML we define a Data Template for Widget One so the Treeview knows how we want the widget to be displayed as well as what data to use. A label will be used for the name, text boxes for Color and State of Mind as well as a button. The button will be for the user to select the Widget from the list of Widgets in the TreeView. Once selected the Xceed PropertyGrid will be assigned the data context of the selected Widget. The Data Template will be defined under the TreeView.Resources tag.
  1. /// <summary>  
  2. /// Creates a new Widget One  
  3. /// </summary>  
  4. /// <param name="sender"></param>  
  5. /// <param name="e"></param>  
  6. private void CreateOne_Click(object sender, RoutedEventArgs e)  
  7. {  
  8.     string str_NewWidgetName = "Widget One - " + list_WidgetOne.Count.ToString();  
  9.     var newWidget = new widgetOne();  
  10.     newWidget.WidgetName = str_NewWidgetName;  
  11.     newWidget.WidgetState = "Happy";      
  12.         // Set to default  
  13.     newWidget.WidgetColor = "Black";      
  14.         // Set to default  
  15.     newWidget.EnumBinding = (SystemType) systype_ProgramMeasurement;  
  16.     list_WidgetOne.Add(newWidget);  
  17.   
  18.     UpdateTreeView();  
  19.     LblStatus.Content = "New Widget One Created: " + newWidget.WidgetCreated;  
  20. }  
  21.   
  22. <!--Widget One Data Template-->  
  23. <DataTemplate DataType="{x:Type local:widgetOne}">  
  24.     <StackPanel Orientation="Vertical"   
  25.     Margin="1,1,1,1" MinWidth="200"   
  26.     Width="Auto" MinHeight="90"   
  27.     Height="Auto">  
  28.         <Border BorderBrush="Black"   
  29.         BorderThickness="1"   
  30.         Padding="2" Margin="2">  
  31.         <Grid>  
  32.             <Grid.RowDefinitions>  
  33.               <RowDefinition Height="30" />  
  34.               <RowDefinition Height="25" />  
  35.               <RowDefinition Height="25" />  
  36.               <RowDefinition Height="25" />  
  37.             </Grid.RowDefinitions>  
  38.             <Grid.ColumnDefinitions>  
  39.               <ColumnDefinition Width="Auto" />  
  40.               <ColumnDefinition Width="Auto" />  
  41.             </Grid.ColumnDefinitions>  
  42.             <Label Grid.Row="0"   
  43.             Grid.ColumnSpan="2" Foreground="Yellow"   
  44.             Background="Transparent"   
  45.             FontStyle="Italic" FontFamily="Copperplate Gothic"   
  46.             FontSize="14">Widget One</Label>  
  47.             <Button Grid.Row="1"   
  48.             Grid.ColumnSpan="2" Name="WidgetName"   
  49.             Content="{Binding Path=WidgetName}"   
  50.             Height="22" Width="Auto"   
  51.             Foreground="Yellow" FontFamily="Copperplate Gothic"   
  52.             Click="WidgetName_Click">  
  53.                 <Button.Background>  
  54.                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">  
  55.                       <GradientStop Color="#FFff0099" Offset="0" />  
  56.                       <GradientStop Color="#FF993366" Offset="1" />  
  57.                    </LinearGradientBrush>  
  58.                 </Button.Background>  
  59.              </Button>  
  60.              <TextBlock Grid.Row="2"   
  61.              Grid.Column="0" FontWeight="Bold"   
  62.              Text="Selected Color:"  Foreground="Yellow"   
  63.              Background="Transparent" FontFamily="Copperplate Gothic" />  
  64.              <TextBlock Grid.Row="2"   
  65.              Grid.Column="1" Text="{Binding Path=WidgetColor}"   
  66.              Margin="5,0,0,0" Foreground="Yellow"   
  67.              Background="Transparent" FontFamily="Copperplate Gothic" />  
  68.              <TextBlock Grid.Row="3"   
  69.              Grid.Column="0" FontWeight="Bold"   
  70.              Text="State Of Mind:" Foreground="White"   
  71.              Background="Transparent" FontFamily="Copperplate Gothic" />  
  72.              <TextBlock Grid.Row="3"   
  73.              Grid.Column="1" Text="{Binding Path=WidgetState}"   
  74.              Margin="5,0,0,0" Foreground="White"   
  75.              Background="Transparent" FontFamily="Copperplate Gothic" />  
  76.         </Grid>  
  77.         </Border>  
  78.      </StackPanel>  
  79. </DataTemplate>  
So far we have only discussed Widget One, yet the program has three different Widgets, and there can be any number of each Widget. So here is the complete UpdateTreeView routine. It creates three branch nodes, one for each Widget and populates the TreeView Node with the corresponding list of Widgets. Of course Widget's are non-sensical, it is just for demonstration purposes. In real life, this technique could be applied to many things, for example a Lumber Yard Inventory program or even an Auto Parts Database. The possibilities are limitless.
  1. /// <summary>  
  2. /// Updates the items in the Treeview  
  3. /// This routine will add each of the created Widgets for Widget One,   
  4. /// Two, and Three; each under their own branch   
  5. /// Widget branches are expanded by default  
  6. /// </summary>  
  7. private void UpdateTreeView()  
  8. {  
  9.     TreeViewItem tvi_Widget = null;  
  10.     // Clear the treeview before update  
  11.     _treeView.ItemsSource = null;  
  12.     _treeView.Items.Clear();  
  13.   
  14.     // Load up the Treeview - First up Widget One  
  15.     if (list_WidgetOne.Count > 0)                     
  16.     // Make sure there are widgets to add before processing  
  17.     {  
  18.         tvi_Widget = new TreeViewItem();  
  19.         tvi_Widget.Header = "Created Widget One(s)";  
  20.         _treeView.Items.Add(tvi_Widget);  
  21.         foreach (var w_Widget in list_WidgetOne)  
  22.         {  
  23.             tvi_Widget.Items.Add(w_Widget);  
  24.         }  
  25.         tvi_Widget.IsExpanded = true;  
  26.     }  
  27.   
  28.     // Now load Widget Two  
  29.     if (list_WidgetTwo.Count > 0)                     
  30.     // Make sure there are widgets to add before processing  
  31.     {  
  32.         tvi_Widget = new TreeViewItem();  
  33.         tvi_Widget.Header = "Created Widget Two(s)";  
  34.         _treeView.Items.Add(tvi_Widget);  
  35.         foreach (var w_Widget in list_WidgetTwo)  
  36.         {  
  37.             tvi_Widget.Items.Add(w_Widget);  
  38.         }  
  39.         tvi_Widget.IsExpanded = true;  
  40.     }  
  41.               
  42.     // Now load Widget Three  
  43.     if (list_WidgetThree.Count > 0)                 
  44.     // Make sure there are widgets to add before processing  
  45.     {  
  46.         tvi_Widget = new TreeViewItem();  
  47.         tvi_Widget.Header = "Created Widget Three(s)";  
  48.         _treeView.Items.Add(tvi_Widget);  
  49.         foreach (var w_Widget in list_WidgetThree)  
  50.         {  
  51.             tvi_Widget.Items.Add(w_Widget);  
  52.         }  
  53.         tvi_Widget.IsExpanded = true;  
  54.     }  
  55.     // Refresh the treeview  
  56.     _treeView.Items.Refresh();  
  57.     _treeView.UpdateLayout();  
  58.   
  59. }  // End UpdateTreeView()  
In the case of this program where I am using the Xceed PropertyGrid, there is a routine to update the grid using two tracking variables. One to hold the current index of the Widget selected, of course, we don't know which Widget that index belongs to; so the other variable is a tracker of which Widget is the active Widget. All Widgets have a button associated with each which call the same routine:
 
WidgetName_Click() Each Widget Name is unique, it is simply a matter of matching the name of the selected Widget. Once matched the index of the Widget and which Widget type are saved to the tracking variables.
  1. // Used to track which Widget is currently selected  
  2. public enum WidgetTypes  
  3. {  
  4.      WidgetOne = 1,  
  5.      WidgetTwo,  
  6.      WidgetThree  
  7. }  
  8. // Set to default of Widget one  
  9. public static WidgetTypes widgtype_Widget = WidgetTypes.WidgetOne;    
  10. public int int_CurrentWidgetIndex = 0;  
  11. .  
  12. .  
  13. .  
  14. /// <summary>  
  15. /// A Widget was clicked in the Treeview, determine which one   
  16. /// and display the it's properties to the property grid  
  17. /// and set the status label to the name of the Widget  
  18. /// </summary>  
  19. /// <param name="sender"></param>  
  20. /// <param name="e"></param>  
  21. private void WidgetName_Click(object sender, RoutedEventArgs e)  
  22. {  
  23.     // Index of the widget selected in the treeview  
  24.     int int_widget_index = -1;    
  25.     // This will contain the Widget Name  
  26.     string string_Widget = e.Source.ToString();  
  27.     // Strips windows excess stuff from source name    
  28.     string_Widget = string_Widget.Remove(0, 32);    
  29.   
  30.     // Determine which Widget was clicked then search the collection for the correct one  
  31.     // Widget One Search  
  32.     if (string_Widget.Contains("One"))                
  33.     {  
  34.         for (int kLoop = 0; kLoop < list_WidgetOne.Count(); kLoop++)  
  35.         {  
  36.             if (null != list_WidgetOne[kLoop])  
  37.             {  
  38.                 if (list_WidgetOne[kLoop].WidgetName == string_Widget)   
  39.                      { int_widget_index = kLoop; break; }  
  40.             }  
  41.         }  
  42.         // If Widget found, bind to property grid  
  43.         if (int_widget_index > -1) void_ShowWidgetOneProperties(int_widget_index);  
  44.     }  
  45.     // Widget Two search  
  46.     else if (string_Widget.Contains("Two"))           
  47.     {  
  48.         // Otherwise check Widget Two now  
  49.         for (int kLoop = 0; kLoop < list_WidgetTwo.Count(); kLoop++)  
  50.         {  
  51.             if (null != list_WidgetTwo[kLoop])  
  52.             {  
  53.                 if (list_WidgetTwo[kLoop].WidgetName == string_Widget)   
  54.                      { int_widget_index = kLoop; break; }  
  55.             }  
  56.         }  
  57.         // If Widget found, bind to property grid  
  58.         if (int_widget_index > -1) void_ShowWidgetTwoProperties(int_widget_index);  
  59.     }  
  60.     // Widget Three search  
  61.     else if (string_Widget.Contains("Three"))             
  62.     {  
  63.         // Check Widget Three now  
  64.         for (int kLoop = 0; kLoop < list_WidgetThree.Count(); kLoop++)  
  65.         {  
  66.             if (null != list_WidgetThree[kLoop])  
  67.             {  
  68.                 if (list_WidgetThree[kLoop].WidgetName == string_Widget)   
  69.                      { int_widget_index = kLoop; break; }  
  70.             }  
  71.         }  
  72.         if (int_widget_index > -1) void_ShowWidgetThreeProperties(int_widget_index);  
  73.     }  
  74.     else  
  75.     {  
  76.         //* ERROR *//  
  77.     }  
  78. }   // End WidgetName_Click()  
  79.   
  80. /// <summary>  
  81. /// Shows the selected Widget One in the Property Grid  
  82. /// </summary>  
  83. /// <param name="intWidgetIndex"></param>  
  84. public void   
  85.      void_ShowWidgetOneProperties(int intWidgetIndex)  
  86. {  
  87.     this.DataContext = list_WidgetOne[intWidgetIndex];  
  88.     // Track the index of the currently selected widget one  
  89.     int_CurrentWidgetIndex = intWidgetIndex;  
  90.     // Widget One is selected      
  91.     widgtype_Widget = WidgetTypes.WidgetOne;      
  92. }  
  93.   
  94. public void   
  95.      void_ShowWidetTwoProperties(int intWidgetIndex)...  
  96. public void   
  97.      void_ShowWidgetThreeProperties(int intWidgetIndex)...  
One last thing I need to talk about before I just dump the code out there. How do you change Measurement Systems and measurement units within a system? There are four combo boxes defined in the program, one for switching between English (US) and Metric (SI) and one combo box for Length, Area, and Volume respectively. Below is listed the complete XAML and event code for the Bucket System - i.e. changing Measurement Systems.
 
We keep track of the selected Measurement System with systype_ProgramMeasurement then the Widgets are updated by setting each Widget to the selected measurement system, and fire the TypeChanged event for each Widget which will update all measurements within the Widgets. The PropertyGrid is updated for the current Widget assigned to it's DataContext. Finally, the combos are updated to show the default measurement unit for Length, Area, and Volume in the selected measurement system.
 
For example, if the Metric System is selected the Length combo is set to the default of "centimeters (SI)". Each of the other combos are updated respectively to their default value as the selected item. All Length Values of Widget One(s) will be converted to the current system.
  1. <!--Buckets Control-->  
  2. <ComboBox Name="cmbBucketSystem"   
  3.     Grid.Row="1"   
  4.     Grid.Column="2"   
  5.     Height="30"   
  6.     Width="Auto"   
  7.     Margin="2,2,2,0"   
  8.     VerticalAlignment="Top"   
  9.     FontFamily="Copperplate Gothic"   
  10.     SelectionChanged="cmbBucketSystem_SelectionChanged"   
  11.     Foreground="Yellow"   
  12.     GotFocus="cmbBucketSystem_GotFocus"   
  13.     LostFocus="cmbBucketSystem_LostFocus">  
  14.        <ComboBox.Background>  
  15.            <LinearGradientBrush   
  16.                        EndPoint="0.5,1"  
  17.                        StartPoint="0.5,0">  
  18.                <GradientStop Color="#FFff0099"  
  19.                     Offset="0" />  
  20.                <GradientStop Color="#FF993366"  
  21.                     Offset="1" />  
  22.            </LinearGradientBrush>  
  23.        </ComboBox.Background>  
  24.        <ComboBoxItem Content="English System"   
  25.        IsSelected="True"></ComboBoxItem>  
  26.        <ComboBoxItem Content="Metric System"></ComboBoxItem>  
  27. </ComboBox>  
  28.   
  29. /// <summary>  
  30. /// Bucket Measurement System - choose between English or Metric  
  31. /// </summary>  
  32. /// <param name="sender"></param>  
  33. /// <param name="e"></param>  
  34. private void cmbBucketSystem_SelectionChanged  
  35.     (object sender, SelectionChangedEventArgs e)  
  36. {  
  37.     ComboBoxItem cbi_SelectedUnit = (ComboBoxItem)cmbBucketSystem.SelectedItem;  
  38.     string str_Content = cbi_SelectedUnit.Content.ToString();  
  39.     if (str_Content.Contains("English System"))   
  40.         systype_ProgramMeasurement = SystemType.UnitedStates;  
  41.     else   
  42.         systype_ProgramMeasurement = SystemType.InternationalSystem;  
  43.   
  44.     UpdateAllWidgets(); // Update all widgets for the measurement system change  
  45.     UpdatePropertyGrid();  // Update the current item in the property grid for the measurement system change  
  46.     UpdateMeasurementCombos();  // Update the measurement combo boxes for the update              
  47. }  
For each of the Widget collections it is just a matter of cycling through each and set EnumBinding to the selected system and firing the TypeChanged() method.
  1. /// <summary>  
  2. /// Update all the Widgets as to the measurement system change  
  3. /// </summary>  
  4. private void UpdateAllWidgets()  
  5. {  
  6.     // Update Widget One collection  
  7.     for (int kLoop = 0; kLoop < list_WidgetOne.Count(); kLoop++)  
  8.     {  
  9.         list_WidgetOne[kLoop].EnumBinding = (SystemType)systype_ProgramMeasurement;  
  10.         list_WidgetOne[kLoop].TypeChanged();  
  11.     }  
  12.     // Update Widget Two collection  
  13.     for (int kLoop = 0; kLoop < list_WidgetTwo.Count(); kLoop++)  
  14.     {  
  15.         list_WidgetTwo[kLoop].EnumBinding = (SystemType)systype_ProgramMeasurement;  
  16.         list_WidgetTwo[kLoop].TypeChanged();  
  17.     }  
  18.     // Update Widget Three collection  
  19.     for (int kLoop = 0; kLoop < list_WidgetThree.Count(); kLoop++)  
  20.     {  
  21.         list_WidgetThree[kLoop].EnumBinding = (SystemType)systype_ProgramMeasurement;  
  22.         list_WidgetThree[kLoop].TypeChanged();  
  23.     }  
  24. }  // End UpdateAllWidgets()  
Updating the combo boxes to the new units. To avoid null instances the code is wrapped with a flag that is set to true when the program launches. The WindowLoaded() event sets the flag to false.
  1. /// <summary>  
  2. /// Routine updates the Measurement System Combos when there is a change in the Measurement System  
  3. /// </summary>  
  4. private void UpdateMeasurementCombos()  
  5. {  
  6.     if (!bool_StartingUp)  
  7.     {  
  8.         // Use Widget One to expose the current measurement system for length  
  9.         widgetOne widgetone_Temp = new widgetOne();  
  10.         widgetone_Temp.EnumBinding = (SystemType)systype_ProgramMeasurement;  
  11.         cmbLengthBuckets.SelectedItem = widgetone_Temp.WidgetLengthBucket;  
  12.   
  13.         // Use Widget Two to expose the current area measurement unit  
  14.         widgetTwo widgettwo_Temp = new widgetTwo();  
  15.         widgettwo_Temp.EnumBinding = (SystemType)systype_ProgramMeasurement;  
  16.         cmbAreaBuckets.SelectedItem = widgettwo_Temp.WidgetAreaBucket;  
  17.   
  18.         // Use Widget Three to get the current volume measurement unit  
  19.         widgetThree widgetthree_Temp = new widgetThree();  
  20.         widgetthree_Temp.EnumBinding = (SystemType)systype_ProgramMeasurement;  
  21.         cmbVolumeBuckets.SelectedItem = widgetthree_Temp.WidgetVolumeBucket;  
  22.     }  
  23. }  // End UpdateMeasurementCombos  
For the Area, Length, and Volume combo boxes, those we will bind in the XAML and use a Selection Change event to provide updates for the new unit selected. We bind the combo boxes directly to the Buckets.Instance and the appropriate unit for the bucket - in the case of Length, it is the LengthBuckets as the ItemsSource and the selected item is the LengthDisplayBucket, the default is "inches (US)". I'll only show one here since the code is the same for each of the other unit selection combo boxes and will be included in the source download.
  1. <ComboBox Name="cmbLengthBuckets" Grid.Row="0"   
  2.     Grid.Column="1" VerticalAlignment="Center"   
  3.     Margin="2,2,2,2" DataContext="{Binding Source={x:Static local:Buckets.Instance}}"   
  4.     ItemsSource="{Binding LengthBuckets}" SelectedItem="{Binding LengthDisplayBucket}"   
  5.     Foreground="Yellow" FontFamily="Copperplate Gothic"   
  6.     GotFocus="cmbLengthBuckets_GotFocus"   
  7.     LostFocus="cmbLengthBuckets_LostFocus"   
  8.     SelectionChanged="cmbLengthBuckets_SelectionChanged">  
  9.     <ComboBox.Background>  
  10.         <LinearGradientBrush   
  11.             EndPoint="0.5,1"  
  12.             StartPoint="0.5,0">  
  13.             <GradientStop Color="#FFff0099"  
  14.             Offset="0" />  
  15.             <GradientStop Color="#FF993366"  
  16.             Offset="1" />  
  17.         </LinearGradientBrush>  
  18.     </ComboBox.Background>  
  19. </ComboBox>  
  20.   
  21. /// <summary>  
  22. /// Handles the changing of Length Measurement Units  
  23. /// </summary>  
  24. /// <param name="sender"></param>  
  25. /// <param name="e"></param>  
  26. private   
  27. void cmbLengthBuckets_SelectionChanged(object sender, SelectionChangedEventArgs e)  
  28. {  
  29.     // Avoid doing this if Starting up  
  30.     if (bool_StartingUp) return;  
  31.   
  32.     bool bool_Update = false;  
  33.   
  34.     // English Units Indexes = 0 (in) 1 (ft)  
  35.     // Metric Units Indexes = 2 (cm) 3 (mm) 4 (m)  
  36.     if (systype_ProgramMeasurement == SystemType.UnitedStates)  
  37.     {  
  38.         if (cmbLengthBuckets.SelectedIndex >= 0 && cmbLengthBuckets.SelectedIndex <= 1)  
  39.         {  
  40.             bool_Update = true;  
  41.         }  
  42.         else  
  43.         {  
  44.             cmbLengthBuckets.SelectedIndex = 0; // Reset to default in (US)  
  45.         }  
  46.     }  
  47.     else  // Metric system being used  
  48.     {  
  49.         if (cmbLengthBuckets.SelectedIndex >= 2 && cmbLengthBuckets.SelectedIndex <= 4)  
  50.         {  
  51.             bool_Update = true;  
  52.         }  
  53.         else  
  54.         {  
  55.             cmbLengthBuckets.SelectedIndex = 2; // Reset to default cm (SI)  
  56.         }  
  57.     }  
  58.     // Check if update is needed...  
  59.     if (bool_Update)  
  60.     {  
  61.         lb_LengthBucket = (Buckets.Bucket)cmbLengthBuckets.SelectedItem;  
  62.         Buckets.Instance.LengthDisplayBucket =   
  63.             Buckets.Instance.LengthBuckets[cmbLengthBuckets.Items.IndexOf(lb_LengthBucket)];  
  64.         UpdatePropertyGrid();  
  65.     }  
  66. }  // End cmbLengthBuckets_SelectionChanged()  
Thanks for bearing with me, I know it was a long article but hopefully I've made some sense out of complex classes and a bit more on WPF. The source code is located here: Widget Manager
 
End Of Line Man!