Tuesday, November 23, 2010

A Behavior for Remembering Settings

I have been unusually busy for the last couple of months and have been neglecting my blog. Things are still pretty busy, but I hope to squeeze out a few posts if I can.

In this post I’m going to describe a behavior that you can attach to your controls and bind to a property on that control. The behavior will monitor the value of that property and store any changes to it in Isolated Storage. The next time you start the application the behavior will look in Isolated Storage and load the value if it can find it, then restore the value to the control. This is a handy behavior for things like grid splitters, sliders, or similar types of controls (dial’s too!) where you want any changes the user makes to be remembered the next time they run your app.

Demo Application

You can grab the behavior here. And this is the behavior in action:

If you refresh this page, the application will remember the opacity value for the border control, the position of the grid splitter, and the selected index of the list.

Using the behavior looks like this:

   1: <Border x:Name="border">
   2:     <i:Interaction.Behaviors>
   3:         <TotalRecall:TotalRecallBehavior Binding="{Binding Opacity, ElementName=border}"/>
   4:     </i:Interaction.Behaviors>
   5: </Border>

There are two things worth discussing about this behavior; the Binding to a control’s property and the the storing/retrieving using Isolated Storage.


Binding to a Control’s Property


The XAML above shows the TotalRecallBehavior being used to remember the opacity of the border element. Internally, the behavior needs to know when the property being bound to (in this instance it is the Border.Opacity property) changes value so it can store it away for next time. To do this, the behavior has an internal DependencyProperty called BindingValueProperty that it uses to create another two-way binding to the same property on the control. BindingValueProperty has an event handler for when the value changes so it can store the new value in Isolated Storage.


The code that uses the behavior’s Binding property to set up a private two-way binding looks like this:



   1: private void CreateBindings()
   2: {
   3:     var expression = this.ReadLocalValue(BindingProperty) as BindingExpression;
   4:     var watcher = new Binding();
   5:     if (expression.ParentBinding.Source != null)
   6:     {
   7:         watcher.Source = expression.ParentBinding.Source;
   8:     }
   9:     else
  10:     {
  11:         watcher.ElementName = expression.ParentBinding.ElementName;
  12:     }
  13:  
  14:     watcher.Path = new PropertyPath(expression.ParentBinding.Path.Path);
  15:     watcher.Mode = BindingMode.TwoWay;
  16:     BindingOperations.SetBinding(this, BindingValueProperty, watcher);
  17:  
  18:     this.recallValue = new RecallValue(this.AssociatedObject.GetType().Name + "." + 
  19:         AssociatedObject.Name + "." + expression.ParentBinding.Path.Path);
  20: }

The call to “this.ReadLocalValue(BindingProperty) as BindingExpression” is exactly what UIElement.GetBindingExpression() does, but the Behavior is not a UIElement so I just make the same call. Getting hold of the BindingExpression is important since it gives us access to the correct binding source and property path. The BindingExpression only exists on the source object of the binding (even if it is two-way), so on line 2 of the XAML snippet the binding is set on the behavior and must therefore be read from the behavior with the call to ReadLocalValue (line 16 of the C# code above). If you try and read the BindingExpression on the element being bound to you will get null returned.


The behavior sets up the binding and creates an instance of the RecallValue class. I have future plans for the RecallValue class, but for now it is simply a wrapper around a string value.


Using the ApplicationSettings on IsolatedStorage


You can use isolated storage to store all kinds of information on the user’s local machine. You can read all about local storage in Silverlight here. One of the less frequently covered uses of Isolated Storage is the IsolatedStorageSettings.ApplicationSettings static property. This property is an IDictionary and has an indexer you can use to read and write with. These are very handy for storing atomic name-value pairs; exactly the kind of thing I want to do with this behavior.


The code that read’s the value from isolated storage is as follows:



   1: if (IsolatedStorageSettings.ApplicationSettings.Contains(this.recallValue.Key))
   2: {
   3:     this.recallValue.Value = (string)IsolatedStorageSettings.ApplicationSettings[this.recallValue.Key];
   4: }
   5: if (null != this.recallValue.Value)
   6: {
   7:     this.SetValue(BindingValueProperty, this.recallValue.Value);
   8: }

The value is read from isolated storage if it exists, and set as the value for our private BindingValueProperty dependency property, which is by now two-way bound to the control’s property. The IsolatedStorageSettings class takes care of creating a location in isolated storage for the values so there’s no other code needed for setting up file streams and creating directories etc. 

Similarly, the code for writing to the isolated storage settings is as follows:


IsolatedStorageSettings.ApplicationSettings[behavior.recallValue.Key] = behavior.recallValue.Value;


The rest of the behavior is just plumbing.


Future Improvements


As I said earlier, I have plans for the RecallValue class. I want to support the saving/restoring of complex objects and values that don’t parse easily to and from string.


I would also like to improve the design-time experience – the Binding type doesn’t play very nice with Blend’s element binding design tool, so I can’t use the CustomPropertyValueEditor attribute. I would really like the design time property editor for the Binding to list the properties on the attached object the same way the ChangePropertyAction does.


Summary


In this post I’ve described a behavior that remembers the value of a property on the control it is attached to. I described the use of the BindingExpression class to create a private two-way binding, and described how to use the IsolatedStorageSettings.ApplicationSettings dictionary. In the code download there are examples of the behavior being used on different kinds of controls and properties.

7 comments:

  1. Hi its working fine with example but i tried following example and its not working

    Rectangle x:Name="Rect"
    Margin="100 10 50 120"
    Height="100"
    Width="100"
    Canvas.Left="{Binding ElementName=LeftPositionTextBox, Path=Text, Mode=TwoWay}" Fill="Aqua"
    Canvas.Top="{Binding ElementName=TopPositionTextBox, Path=Text, Mode=TwoWay}">




    </Rectangle


    awaiting your reply ASAP

    ReplyDelete
  2. one more thing what would i do if i am adding my controls from Code Behind

    ReplyDelete
  3. h, I'm guessing there is some behavior code in your sample that is not showing up in the comments.

    If you are trying to bind the behavior to Canvas.Left then user the following syntax on the behavior (note the parentheses):

    {Binding (Canvas.Left), Path=Rect}

    For code behind you need to add the behavior to the rectangle's Interaction.Behaviors attached property (you may have to create the BehaviorCollection) and set the binding on the behavior by hand.

    Have a google for how to do these things, and if you get stuck let me know and I will do a short blog post on how to do it.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Can you give me rough idea about the interaction behavior you are talking.
    actually i want to persist Canvas's Children & ListBox's ItemsSource.I am doing something like below

    Canvas x:Name="myCanvas" Background="Chocolate" Grid.Column="1">
    i:Interaction.Behaviors>
    TotalRecall:Recaller Data="{Binding Children,ElementName=myCanvas,Mode=TwoWay}"

    </Canvas

    I am doing all code when element is loaded

    ReplyDelete
  6. The behavior is not designed for persisting whole collections of child objects, just single properties with values that can be converted to a string and back. It will not work with the Canvas.Children property since that is a read only property of type VisualCollection.

    It would possibly work with a ListBox.ItemsSource since that is a writable property, but you would have to also provide a custom converter to translate to and from string and IEnumerable.

    The Interaction.Behaviors property is an attached property that you are already using in the XAML to contain the TotalRecall behavior. For more info, follow this link

    ReplyDelete