Saturday, June 18, 2011

Binding Dependencies

A common problem I've noticed, even on the SDK team, was when you have one property dependent on another property (or a set of other properties), and you need that property to be Bindable. For example, in spark.components.VideoPlayer, volume is dependent on the videoDisplay.volume property. Usually to make such a property bindable, you add an event listener to know when your dependent property changes. In this case, if you check out VideoPlayer.partAdded(), you can see code like that. However, when I started using the presentation model pattern, this issue seemed to keep coming up quite frequently.
Let's take a simple example where someone is placing an order. If that person has a billing address and a shipping address in California, we need to show them some extra questions because of extra regulations in California.
Our made up model looks like:

public class Order
{
    [Bindable]
    public var billingState:String;
    
    [Bindable]
    public var shippingState:String;
}      
  
And in the View, we want to do something like:

<s:Form>
    <s:FormItem label="Billing State">
        <s:DropDownList selectedItem="@{ presentationModel.order.billingState }">
            <s:ArrayList>
                <fx:String>AL</fx:String>
                <fx:String>AS</fx:String>
                <fx:String>AZ</fx:String>
                <fx:String>AR</fx:String>
                <fx:String>CA</fx:String>
                <fx:String>CO</fx:String>
                <fx:String>CT</fx:String>
                <fx:String>DE</fx:String>
            </s:ArrayList>
        </s:DropDownList>
    </s:FormItem>
    
    <s:FormItem label="Shipping State">
        <s:DropDownList selectedItem="@{ presentationModel.order.shippingState }">
            <s:ArrayList>
                <fx:String>AL</fx:String>
                <fx:String>AS</fx:String>
                <fx:String>AZ</fx:String>
                <fx:String>AR</fx:String>
                <fx:String>CA</fx:String>
                <fx:String>CO</fx:String>
                <fx:String>CT</fx:String>
                <fx:String>DE</fx:String>
            </s:ArrayList>
        </s:DropDownList>
    </s:FormItem>
    
    <s:FormItem label="California Question1"
                includeInLayout="{ presentationModel.order.shippingState == 'CA' &amp;&amp; presentationModel.order.billingState == 'CA' }"
                visible="{ presentationModel.order.shippingState == 'CA' &amp;&amp; presentationModel.order.billingState == 'CA' }">
        <s:TextInput />
    </s:FormItem>
    
    <s:FormItem label="California Question2"
                includeInLayout="{ presentationModel.order.shippingState == 'CA' &amp;&amp; presentationModel.order.billingState == 'CA' }"
                visible="{ presentationModel.order.shippingState == 'CA' &amp;&amp; presentationModel.order.billingState == 'CA' }">
        <s:TextInput />
    </s:FormItem>
</s:Form>      
  
Now the same code: presentationModel.order.shippingState == 'CA' &amp;&amp; presentationModel.order.billingState == 'CA' is repeated 4 times, the ampersands have to be escaped, and overall it's just quite ugly. Now, one of the neat tricks I've learned is that you can spruce it up by doing something like:

<fx:Declarations>
    <fx:Boolean id="showCaliforniaQuestions">
        {presentationModel.order.shippingState == "CA" &amp;&amp; 
        presentationModel.order.billingState == "CA"}
    </fx:Boolean>
</fx:Declarations>

...

<s:FormItem label="California Question1"
            includeInLayout="{ showCaliforniaQuestions }"
            visible="{ showCaliforniaQuestions }">
    <s:TextInput />
</s:FormItem>

<s:FormItem label="California Question2"
            includeInLayout="{ showCaliforniaQuestions }"
            visible="{ showCaliforniaQuestions }">
    <s:TextInput />
</s:FormItem>
  
That definitely works and is much cleaner than before; however, that means we have some business logic in our View that is extremely hard to test. That logic should really be in the presentation model. If we try to move it into the Presentation Model, we'll end up with something that looks like:

<s:FormItem label="California Question1"
            includeInLayout="{ presentationModel.showCaliforniaQuestions }"
            visible="{ presentationModel.showCaliforniaQuestions }">
    <s:TextInput />
</s:FormItem>

<s:FormItem label="California Question2"
            includeInLayout="{ presentationModel.showCaliforniaQuestions }"
            visible="{ presentationModel.showCaliforniaQuestions }">
    <s:TextInput />
</s:FormItem>
  

public class MyPM
{
    public function MyPM()
    {
        super();
    }
    
    [Bindable]
    public var order:Order = new Order();
    
    public function get showCaliforniaQuestions():Boolean
    {
        return order.shippingState == "CA" && order.billingState == "CA";
    }
}
  
However, doing that doesn't work. The reason is because showCaliforniaQuestions isn't Bindable. Now, to make it Bindable, we need to add Bindable metadata on top, add event listeners (or use a BindingWatcher) to let us know when the shipping state or the billing state changes, and dispatch a new binding event when we get notified that the shipping or billing state changes. This turns out to be quite a lot of extra, ugly code, which means most people just end up keeping this logic in the View because practically it's just too much work and too ugly to move it to the Presentation Model. This is all fine, but this kept issue kept cropping up in practice, so I finally sat down to come up with a more palatable option.
One of the very cool things about Parsley is that you can add your own custom Metadata, and Parsley will help process it for you. Even though I'm really new to Parsley, it turns out that this is relatively easy to do. So I decided to go ahead and add some custom metadata to help deal with this. What's really neat is that I added a Parsley processor for Bindable metadata. I added a new property on that metadata, which allows you to define binding dependencies--basically Bindings that are dependent on other properties. In our particular example, here's what it looks like:

[Bindable(event="showCaliforniaQuestionsChanged",dependentProperty="order.shippingState")]
[Bindable(event="showCaliforniaQuestionsChanged",dependentProperty="order.billingState")]
public function get showCaliforniaQuestions():Boolean
{
    return order.shippingState == "CA" && order.billingState == "CA";
}
All we did was declare the showCaliforniaQuestions as Bindable, where that binding is dependent on the order.shippingState and order.billingState properties. The Parsley metadata processor will step in and handle everything else. I personally like this solution a lot as it piggy-backs off of the Flex SDK framework's Bindable metadata and just extends it for our purpose. It's easy to use and pushes our logic into Presentation Model, which makes our code cleaner and more importantly, testable.
The main downside to this approach is that we're exposed to spelling mistakes or later refactorings since the dependentProperty in the Bindable metadata is not checked by the compiler. Ideally, this would be something we could add to the ActionScript compiler so that it could inspect the getter and pick out all the bindable variables. However, we don't have that, and I find this pattern really convinient for me. The code for all of this can be found in the attached Flash Builder project--feel free to use it.

No comments: