Condividi tramite


WF4 Design Time AttachedPropertiesService and Attached Properties

I’ve been meaning to throw together some thoughts on attached properties and how they can be used within the designer.  Basically, you can think about attached properties as injecting some additional “stuff” onto an instance that you can use elsewhere in your code.

Motivation

In the designer, we want to be able to have behavior and view tied to interesting aspects of the data.  For instance, we would like to have a view updated when an item becomes selected.  In WPF, we bind the style based on the “isSelectionProperty.”  Now, our data model doesn’t have any idea of selection, it’s something we’d like the view level to “inject” that idea on any model item so that a subsequent view could take advantage of.  You can kind of view Attached Properties as a nice syntactic sugar to not have to keep a bunch of lookup lists around.  As things like WPF bind to the object very well, and not so much a lookup list, this ends up being an interesting model.

To be clear, you could write a number of value converters that take the item being bound, look up in a lookup list somewhere, and return the result that will be used.  The problem we found is that we were doing this in a bunch of places, and we really wanted to have clean binding statements inside our WPF XAML, rather than hiding a bunch of logic in the converters.

How Does it Work

First, some types.

Name Description
AttachedPropertiesService Service in editing context for managing AttachedProperties
AttachedProperty Base attached property type (abstract)
AttachedProperty<T> Strongly typed attached property with interesting getter/setter programmability

in diagram form:

image

One thing that might look a little funny to some folks who have used attached properties in other contexts (WF3, WPF, XAML), is the “IsBrowsable” property.  The documentation is a little sparse right now, but what this will do is determine how discoverable the property is.  If this is set to true, the attached property will show up in the Properties collection of the ModelItem to which the AP is attached.  What this means is that it can show up in the Property grid, you can bind WPF statements directly to it, as if it were a real property of the object.  Attached properties by themselves have no actual storage representation, so these exist as design time only constructs.

Getter/ Setter?

One other thing that you see on the AttachedProperty<T> is the Getter and Setter properties.  These are of type Func<ModelItem,T> and Action<ModelItem,T> respectively.  What these allow you to do is perform some type of computation whenever the get or set is called against the AttachedProperty.  Why is this interesting?  Well, let’s say that you’d like to have a computed value retrieved, such as “IsPrimarySelection” checking with the Selection context item to see if an item is selected.  Or, customizing the setter to either store the value somewhere more durable, or updating a few different values.  The other thing that happens is that since all of these updates go through the ModelItem tree, any changes will be propagated to other listeners throughout the designer.

Looking at Some Code

Here is a very small console based app that shows how you can program against the attached properties.  An interesting exercise for the reader would be to take this data structure, put it in a WPF app and experiment with some of the data binding.

First, two types:

 public class Dog
{
    public string Name { get; set; }
    public string Noise { get; set; }
    public int Age { get; set; }
   
}

public class Cat
{
    public string Name { get; set; }
    public string Noise { get; set; }
    public int Age { get; set; }
}

Ignore no common base type, that actually makes this a little more interesting, as we will see.

Now, let’s write some code.  First, let’s initialize and EditingContext and ModelTreeManager

    1:       static void Main(string[] args)
    2:          {
    3:              EditingContext ec = new EditingContext();
    4:              ModelTreeManager mtm = new ModelTreeManager(ec);
    5:              mtm.Load(new object[] { new Dog { Name = "Sasha", Noise = "Snort", Age = 5 },
    6:                                      new Cat { Name="higgs", Noise="boom", Age=1 } });
    7:              dynamic root = mtm.Root;
    8:              dynamic dog = root[0];
    9:              dynamic cat = root[1];
   10:              ModelItem dogMi = root[0] as ModelItem;
   11:              ModelItem catMi = root[1] as ModelItem;

Note, lines 7-9 will not work in Beta2 (preview of coming attractions). To get lines 10-11 working in beta2, cast root to ModelItemCollection and then use the indexers to extract the values

Now, let’s build an attached property, and we will assign it only to type “dog”

    1:  // Add an attached Property
    2:  AttachedProperty<bool> ap = new AttachedProperty<bool>
    3:  {
    4:      IsBrowsable = true,
    5:      Name = "IsAnInterestingDog",
    6:      Getter = (mi => mi.Properties["Name"].ComputedValue.ToString() == "Sasha"),
    7:      OwnerType = typeof(Dog)
    8:  };
    9:  ec.Services.Publish<AttachedPropertiesService>(new AttachedPropertiesService());
   10:  AttachedPropertiesService aps = ec.Services.GetService<AttachedPropertiesService>();
   11:  aps.AddProperty(ap);
   12:   
   13:  Console.WriteLine("---- Enumerate properties on dog (note new property)----");
   14:  dogMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));
   15:   
   16:  Console.WriteLine("---- Enumerate properties on cat (note  no new property) ----");
   17:  catMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));

Let’s break down what happened here.

  • Line2-8, create an AttachedProperty<bool>
    • We set IsBrowsable to true, we want to see it in the output
    • Name, that’s what it will be projected as
    • OwnerType, we only want this to apply to Dog’s, not Cat’s or Objects or whatever.
    • Finally, Getter, and look what we do here, we operate on the model item to do some computation and return a bool (in this case, we look to see if the name property equals “Sasha”
  • Line 9-11 create an AttachedPropertiesService and add it to the editing context.
  • Lines 13-17 output the properties, and let’s see what that looks like:
 ---- Enumerate properties on dog (note new property)----
  Property : Name
  Property : Noise
  Property : Age
  Property : IsAnInterestingDog
 ---- Enumerate properties on cat (note  no new property) ----
  Property : Name
  Property : Noise
  Property : Age

Ok, so that’s interesting, we’ve injected a new property, only on the dog type.  If I got dogMI.Properties[“IsAnInterestingDog”], I would have a value that I could manipulate (albeit returned via the getter).

Let’s try something a little different:

    1:  AttachedProperty<bool> isYoungAnimal = new AttachedProperty<bool>
    2:  {
    3:      IsBrowsable = false,
    4:      Name = "IsYoungAnimal",
    5:      Getter = (mi => int.Parse(mi.Properties["Age"].ComputedValue.ToString()) < 2)
    6:  };
    7:   
    8:  aps.AddProperty(isYoungAnimal);
    9:   
   10:  // expect to not see isYoungAnimal show up
   11:  Console.WriteLine("---- Enumerate properties on dog  (note isYoungAnimal doesn't appear )----");
   12:  dogMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));
   13:  Console.WriteLine("---- Enumerate properties on cat (note isYoungAnimal doesn't appear )----");
   14:  catMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));
   15:   
   16:  Console.WriteLine("---- get attached property via GetValue ----");
   17:  Console.WriteLine("getting non browsable attached property on dog {0}", isYoungAnimal.GetValue(dogMi));
   18:  Console.WriteLine("getting non browsable attached property on cat {0}", isYoungAnimal.GetValue(catMi));

Let’s break this down:

  • Lines 1-6 create a new attached property
    • IsBrowsable is false
    • No OwnerType being set
    • The Getter does some computation to return true or false
  • Lines 10-14 write out the properties (as above)
  • Lines 17-18 extract the value with AttachedPropertyInstance.GetValue(ModelItem)

Let’s see the output there:

 ---- Enumerate properties on dog  (note isYoungAnimal doesn't appear )----
  Property : Name
  Property : Noise
  Property : Age
  Property : IsAnInterestingDog
 ---- Enumerate properties on cat (note isYoungAnimal doesn't appear )----
  Property : Name
  Property : Noise
  Property : Age
 ---- get attached property via GetValue ----
 getting non browsable attached property on dog False
 getting non browsable attached property on cat True

As we can see, we’ve now injected this behavior, and we can extract the value. 

Let’s get a little more advanced and do something with the setter.  Here, if isYoungAnimal is set to true, we will change the age (it’s a bit contrived, but shows the dataflow on simple objects, we’ll see in a minute a more interesting case).

    1:  // now, let's do something clever with the setter. 
    2:  Console.WriteLine("---- let's use the setter to have some side effect ----");
    3:  isYoungAnimal.Setter = ((mi, val) => { if (val) { mi.Properties["Age"].SetValue(10); } });
    4:  isYoungAnimal.SetValue(cat, true);
    5:  Console.WriteLine("cat's age now {0}", cat.Age);

Pay attention to what the Setter does now.  We create the method through which subsequent SetValue’s will be pushed.  Here’s that output:

 ---- let's use the setter to have some side effect ----
cat's age now 10

Finally, let’s show an example of how this can really function as some nice sugar to eliminate the need for a lot of value converters in WPF by using this capability as a way to store the relationship somewhere (rather than just using at a nice proxy to change a value):

    1:  // now, let's have a browesable one with a setter.
    2:  // this plus dynamics are a mini "macro language" against the model items
    3:   
    4:  List<Object> FavoriteAnimals = new List<object>();
    5:   
    6:  // we maintain state in FavoriteAnimals, and use the getter/setter func
    7:  // in order to query or edit that collection.  Thus changes to an "instance"
    8:  // are tracked elsewhere.
    9:  AttachedProperty<bool> isFavoriteAnimal = new AttachedProperty<bool>
   10:  {
   11:      IsBrowsable = false,
   12:      Name = "IsFavoriteAnimal",
   13:      Getter = (mi => FavoriteAnimals.Contains(mi)),
   14:      Setter = ((mi, val) => 
   15:          {
   16:              if (val)
   17:                  FavoriteAnimals.Add(mi);
   18:              else
   19:              {
   20:                  FavoriteAnimals.Remove(mi);
   21:              }
   22:          })
   23:  };
   24:   
   25:   
   26:  aps.AddProperty(isFavoriteAnimal);
   27:   
   28:  dog.IsFavoriteAnimal = true;
   29:  // remove that cat that isn't there
   30:  cat.IsFavoriteAnimal = false;
   31:  cat.IsFavoriteAnimal = true;
   32:  cat.IsFavoriteAnimal = false;
   33:   
   34:  Console.WriteLine("Who are my favorite animal?");
   35:  FavoriteAnimals.ForEach(o => Console.WriteLine((o as ModelItem).Properties["Name"].ComputedValue.ToString()));

Little bit of code, let’s break it down one last time:

  • Line 14 – Create a setter that acts upon the FavoriteAnimals collection to either add or remove the element
  • Line 28-32 – do a few different sets on this attached property
    • NOTE: you can’t do that in beta2 as the dynamic support hasn’t been turned on.  Rather you would have to do isFavoriteAnimal.SetValue(dogMi, true).
  • Line 35 then prints the output to the console, and as expected we only see the dog there:
 -- Who are my favorite animals?
Sasha

I will attach the whole code file at the bottom of this post, but this shows you how you can use the following:

  • Attached properties to create “computed values” on top of existing types
  • Attached properties to inject a new (and discoverable) property entry on top of the designer data model (in the form of a new property)
  • Using the Setter capability to both propagate real changes to the type, providing a nice way to give a cleaner interface, as well as use it as a mechanism to store data about the object outside of the object, but in a way that gives me access to it such that it seems like the object. 
    • This is some really nice syntactic sugar that we sprinkle on top of things

What do I do now?

Hopefully this post gave you some ideas about how the attached property mechanisms work within the WF4 designer.  These give you a nice way to complement the data model and create nice bindable targets that your WPF Views can layer right on top of.

A few ideas for these things:

  • Use the Setters to clean up a “messy” activity API into a single property type that you then build a custom editor for in the property grid. 
  • Use the Getters (and the integration into the ModelProperty collection) in order to create computational properties that are used for displaying interesting information on the designer surface.
  • Figure out how to bridge the gap to take advantage of the XAML attached property storage mechanism, especially if you author runtime types that look for attached properties at runtime. 
  • Use these, with a combination of custom activity designers to extract and display interesting runtime data from a tracking store

 

Full Code Posting

 using System;
using System.Activities.Presentation;
using System.Activities.Presentation.Model;
using System.Collections.Generic;
using System.Linq;

namespace AttachedPropertiesBlogPosting
{
    class Program
    {
        static void Main(string[] args)
        {
            EditingContext ec = new EditingContext();
            ModelTreeManager mtm = new ModelTreeManager(ec);
            mtm.Load(new object[] { new Dog { Name = "Sasha", Noise = "Snort", Age = 5 },
                                    new Cat { Name="higgs", Noise="boom", Age=1 } });
            dynamic root = mtm.Root;
            dynamic dog = root[0];
            dynamic cat = root[1];
            ModelItem dogMi = root[0] as ModelItem;
            ModelItem catMi = root[1] as ModelItem;
          
            // Add an attached Property
            AttachedProperty<bool> ap = new AttachedProperty<bool>
            {
                IsBrowsable = true,
                Name = "IsAnInterestingDog",
                Getter = (mi => mi.Properties["Name"].ComputedValue.ToString() == "Sasha"),
                OwnerType = typeof(Dog)
            };
            ec.Services.Publish<AttachedPropertiesService>(new AttachedPropertiesService());
            AttachedPropertiesService aps = ec.Services.GetService<AttachedPropertiesService>();
            aps.AddProperty(ap);

            Console.WriteLine("---- Enumerate properties on dog (note new property)----");
            dogMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));

            Console.WriteLine("---- Enumerate properties on cat (note  no new property) ----");
            catMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));


            
            AttachedProperty<bool> isYoungAnimal = new AttachedProperty<bool>
            {
                IsBrowsable = false,
                Name = "IsYoungAnimal",
                Getter = (mi => int.Parse(mi.Properties["Age"].ComputedValue.ToString()) < 2)
            };

            aps.AddProperty(isYoungAnimal);

            // expect to not see isYoungAnimal show up
            Console.WriteLine("---- Enumerate properties on dog  (note isYoungAnimal doesn't appear )----");
            dogMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));
            Console.WriteLine("---- Enumerate properties on cat (note isYoungAnimal doesn't appear )----");
            catMi.Properties.ToList().ForEach(mp => Console.WriteLine(" Property : {0}", mp.Name));

            Console.WriteLine("---- get attached property via GetValue ----");
            Console.WriteLine("getting non browsable attached property on dog {0}", isYoungAnimal.GetValue(dogMi));
            Console.WriteLine("getting non browsable attached property on cat {0}", isYoungAnimal.GetValue(catMi));
            
            
            // now, let's do something clever with the setter. 
            Console.WriteLine("---- let's use the setter to have some side effect ----");
            isYoungAnimal.Setter = ((mi, val) => { if (val) { mi.Properties["Age"].SetValue(10); } });
            isYoungAnimal.SetValue(cat, true);
            Console.WriteLine("cat's age now {0}", cat.Age);

            // now, let's have a browesable one with a setter.
            // this plus dynamics are a mini "macro language" against the model items

            List<Object> FavoriteAnimals = new List<object>();

            // we maintain state in FavoriteAnimals, and use the getter/setter func
            // in order to query or edit that collection.  Thus changes to an "instance"
            // are tracked elsewhere.
            AttachedProperty<bool> isFavoriteAnimal = new AttachedProperty<bool>
            {
                IsBrowsable = false,
                Name = "IsFavoriteAnimal",
                Getter = (mi => FavoriteAnimals.Contains(mi)),
                Setter = ((mi, val) => 
                    {
                        if (val)
                            FavoriteAnimals.Add(mi);
                        else
                        {
                            FavoriteAnimals.Remove(mi);
                        }
                    })
            };
            aps.AddProperty(isFavoriteAnimal);
            dog.IsFavoriteAnimal = true;
            // remove that cat that isn't there
            cat.IsFavoriteAnimal = false;
            cat.IsFavoriteAnimal = true;
            cat.IsFavoriteAnimal = false;
            Console.WriteLine("Who are my favorite animals?");
            FavoriteAnimals.ForEach(o => Console.WriteLine((o as ModelItem).Properties["Name"].ComputedValue.ToString()));
            Console.ReadLine();
        }
    }

    public class Dog
    {
        public string Name { get; set; }
        public string Noise { get; set; }
        public int Age { get; set; }
    }

    public class Cat
    {
        public string Name { get; set; }
        public string Noise { get; set; }
        public int Age { get; set; }
    }
}

Comments

  • Anonymous
    December 06, 2009
    This is great stuff, very cool.  I'm just wondering if there is an easy way to have the properties value serialized out to the xaml?

  • Anonymous
    December 08, 2009
    Another excellent blog post.  Thank you again, Matt! From what I understand, the attached property service/attached properties and viewstate discussed in this, the previous, and the next blog post, are extensibility mechanisms for design time.  There are several interesting use cases I can imagine, including the comment scenario you demonstrated. The "what do I do now?" heading listed a couple of interesting points including: "Figure out how to bridge the gap to take advantage of the XAML attached property storage mechanism, especially if you author runtime types that look for attached properties at runtime. " I'm not entirely clear on what your point was above - is it meant as an exercise to the reader to figure out how to merge the workflow design time properties with runtime properties? I'm (now) interested in learning more about any extensibility model for adding properties to a third party activity (i.e. not a custom activity I write) for runtime use, if something like this is possible.  I'm not sure if your point above is a hint of an approach that might be used to achieve this.  I've post a question in the forums around this (http://social.msdn.microsoft.com/Forums/en-US/wfprerelease/thread/acbb8095-f8dd-4ecd-b733-d6ac7742d152) so hopefully someone can enlighten me about that. Thanks again!

  • Anonymous
    December 08, 2009
    The comment has been removed