Xamarin.Froms: Attached Behaviors

comments edit

To recap, I’m writing a shopping cart app for Windows Phone, Android, and iOS. The purpose of the app is primarily to let me use Forms. Each post will build on top of the previous one.

Last time I styled the app so it looked slick. This week I am going to revisit a problem I had in my Day 2 post, namely the lack of an EventToCommand behavior. A developer named Corrado created a Behaviors library specifically for Xamarin.Forms. This library comes with an EventToCommand behavior out of the box, and lets you create your own.

Recap and Code

This is the seventh post in the series, you can find the rest here:

  • Day 0: Getting Started (blog / code)
  • Day 1: Binding and Navigation (blog / code)
  • Day 2: Frames, Event Handlers, and Binding Bugs (blog / code)
  • Day 3: Images in Lists (blog / code)
  • Day 4: Search and Barcode Scanner (blog / code)
  • Day 5: Dependency Injection (blog / code)
  • Day 6: Styling (blog / code)
  • Day 7: Attached Behaviors (blog / code)
  • Day 8: Writing to Disk (blog / code)
  • Day 9: App and Action Bars (blog / code)
  • Day 10: Native Views (blog / code)

For a full index of posts, including future posts, go to the GitHub project page.

Getting Behaviors

First off, you can check out Corrado’s own blog post about this library. You can also take a look at his code on GitHub, or just grab the library from nuget.

The first thing I did was install the nuget package in my core project (ShoppingCart). I did two things wrong here. First, there are two nuget packages to choose from: Xamarin.Behaviors and Xamarin.Forms.Behaviors. Unintuitively, the correct one to choose is Xamarin.Behaviors. The next mistake I made was that I installed it in just the core project. When I ran up the solution, I saw this error immediately:

System.IO.FileNotFoundException was unhandled by user code
Message=Could not load file or assembly 'Xamarin.Behaviors, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

I realized that the platform projects also need to reference the package. Easy enough.

TL;DR

To install behaviors install the nuget package in your shared project as well as all platform projects:

PM> Install-Package Xamarin.Forms.Behaviors

Using Behaviors

My first use case for behaviors is to remove the ugly event to command code I have in my code behind. Here’s the xaml that I want to get rid of:

<ListView ItemsSource="{Binding Categories.Result}"
    IsGroupingEnabled="false"
    ItemSelected="OnItemSelected">
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell>
        <Label Text="{Binding .}" />
      </ViewCell>
    </DataTemplate>
</ListView>

Specifically, I don’t want the ItemSelected property set to the OnItemSelcted method in the code behind file:

private void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
    var param = e.SelectedItem as string;
    var command = ((CategoriesListViewModel)BindingContext).NavigateToCategory;
 
    if (command.CanExecute(param))
    {
        command.Execute(param);
    }
}

This method casts the context to the view model, grabs the command, casts the SelectedItem into a string to act as the parameter, checks to see if it can call execute, and then calls execute.

First things first, I delete the OnItemSelected method. Gone. No more. Next, I add an EventToCommand behavior in my xaml:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ShoppingCart.Views.CategoriesListPage"
             xmlns:b="clr-namespace:Xamarin.Behaviors;assembly=Xamarin.Behaviors"
             xmlns:local="clr-namespace:ShoppingCart;assembly=ShoppingCart"
             BindingContext="{x:Static local:App.CategoriesListViewModel}"
             BackgroundColor="White">
  <ListView ItemsSource="{Binding Categories.Result}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <TextCell Text="{Binding Name}"
                  Detail="{Binding Count}">
          <b:Interaction.Behaviors>
            <b:BehaviorCollection>
              <b:EventToCommand EventName="Tapped"
                                Command="{Binding NavigateToCategory}"
                                CommandParameter="{Binding Category}" />
            </b:BehaviorCollection>
          </b:Interaction.Behaviors>
        </TextCell>
      </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

There’s a little more going on here than just the behavior, so I’ll explain that first. First off, on line 4, I add the reference to the Behaviors namespace. I also change the DataTemplate from the generic ViewCell to the TextCell. This is mostly just to simplify my layout and because I only recently learned about the TextCell after reading a recent blog on the Xamarin Newsletter. The TextCell lets you create a row in a ListView with a main text field, and a description underneath. I also just realized that the ViewCell and TextCell both already have Command and CommandParameter properties that I could have bound to directly. Evidently I don’t need behaviors for this at all. I’m still going to use behaviors, just so I can play with them a bit. But, if you want to see how to do this without behaviors, check out my list view in the ProductsListPage.

So, now that I have my TextCell, I can use the Interaction.Behaviors attached property and add an EventToCommand behavior. The EventToCommand maps an event on the UI control to an ICommand on the view model. In this case, when the Tapped event of the TextCell is raised, the NavigateToCategory command will be executed. But which NavigateToCategory command? Originally this command existed on the CategoriesListViewModel, but that was when we were in the code behind and our BindingContext was the CategoriesListViewModel. By the time our EventToCommand is created, we are in the DataTemplate and only have access to the individual members of Categories.Results which was originally a list of strings. If we were using WPF, we would have been able to bind to our parent’s context using RelativeSource binding and access the command. RelativeSource binding is not an option in XF. The easiest way around this for me is to change my categories list from strings to CategoryViewModels. Here’s my new view model:

public class CategoryViewModel : BaseViewModel
{
    private readonly Category _category;
 
    public CategoryViewModel(Category category, ICommand navigateCommand)
    {
        _category = category;
        Name = _category.Name;
 
        NavigateToCategory = navigateCommand;
    }
 
    public Category Category { get { return _category; } }
 
    public string Count { get; private set; }
 
    public string Name { get; private set; }
 
    public ICommand NavigateToCategory { get; private set; }
}

The CategoriesListViewModel creates these instances, and just passes the navigate command in. The implementation of the command itself isn’t changed. Truth be told, passing the command in like this is a bit of a hack. It would be cleaner to use the Message Center. That’s a bit out of the scope for this article, perhaps I’ll clean this up next week.

Another thing I’m doing that’s not strictly necessary, is passing in a CommandParameter. I’m just using it here just to show how it can be done. Currently, you can’t pass in the EventArgs as the parameter. There are times when that is useful, so hopefully it’s added some time in the future before I really need it.

What Did I Do Wrong?

Typo in the EventName

At one point in my testing, I had a typo in my EventToCommand where I was trying to bind to a nonexistent event.

<b:EventToCommand EventName="OnTapped"
                  Command="{Binding NavigateCommand}"
                  CommandParameter="{Binding Category}" />

OnTapped doesn’t exist. The correct event name is “Tapped”. This is the error you’ll see if/when you make that mistake:

System.FormatException: Index (zero based) must be greater than or equal to zero and less than the size of the argument list

The exception is confusing until you look at the EventToCommand code and see that there is a small bug in it when it is trying to throw what would be a much more helpful exception.

Typo in the Command

I also had some trouble with typos where I misspelled the name of the command. This was worse. It just silently doesn’t work. Typos are bad.

Creating Behaviors

Xamarin.Forms.Behaviors comes with two behaviors out of the box: EventToCommand which we discussed earlier, and TextChangedBehavior. Even better though, it gives you all the building blocks you need to create behaviors of your own. Suppose you want to have your Entry (text box) animate when you click in it. Something like this:

Windows Phone Animation

Droid Animation

Here’s the behavior that handles this:

using System;
using Xamarin.Behaviors;
using Xamarin.Forms;
 
namespace ShoppingCart.Behaviors
{
    public class AnimateSizeBehavior : Behavior<View>
    {
        public static readonly BindableProperty EasingFunctionProperty = BindableProperty.Create<AnimateSizeBehavior, string>(
            p => p.EasingFunctionName,
            "SinIn",
            propertyChanged: OnEasingFunctionChanged);
 
        public static readonly BindableProperty ScaleProperty = BindableProperty.Create<AnimateSizeBehavior, double>(
            p => p.Scale,
            1.25);
 
        private Easing _easingFunction;
 
        public string EasingFunctionName
        {
            get { return (string)GetValue(EasingFunctionProperty); }
            set { SetValue(EasingFunctionProperty, value); }
        }
 
        public double Scale
        {
            get { return (double)GetValue(ScaleProperty); }
            set { SetValue(ScaleProperty, value); }
        }
 
        protected override void OnAttach()
        {
            this.AssociatedObject.Focused += OnItemFocused;
        }
 
        protected override void OnDetach()
        {
            this.AssociatedObject.Focused -= OnItemFocused;
        }
 
        private static Easing GetEasing(string easingName)
        {
            switch (easingName)
            {
                case "BounceIn": return Easing.BounceIn;
                case "BounceOut": return Easing.BounceOut;
                case "CubicInOut": return Easing.CubicInOut;
                case "CubicOut": return Easing.CubicOut;
                case "Linear": return Easing.Linear;
                case "SinIn": return Easing.SinIn;
                case "SinInOut": return Easing.SinInOut;
                case "SinOut": return Easing.SinOut;
                case "SpringIn": return Easing.SpringIn;
                case "SpringOut": return Easing.SpringOut;
                default: throw new ArgumentException(easingName + " is not valid");
            }
        }
 
        private static void OnEasingFunctionChanged(BindableObject bindable, string oldvalue, string newvalue)
        {
            (bindable as AnimateSizeBehavior).EasingFunctionName = newvalue;
            (bindable as AnimateSizeBehavior)._easingFunction = GetEasing(newvalue);
        }
 
        private async void OnItemFocused(object sender, FocusEventArgs e)
        {
            await this.AssociatedObject.ScaleTo(Scale, 250, _easingFunction);
            await this.AssociatedObject.ScaleTo(1.00, 250, _easingFunction);
        }
    }
}

This is a big file, but not that much is really going on. AnimateSizeBehavior inherits from Behavior. This means that we can apply it to any type of control. It also means that the AssociatedObject property will be of type View. The key methods to look at are the OnAttach and OnDetach.

protected override void OnAttach()
{
    this.AssociatedObject.Focused += OnItemFocused;
}
 
protected override void OnDetach()
{
    this.AssociatedObject.Focused -= OnItemFocused;
}

OnAttach is called when the behavior is added to the the control and OnDetach is called when it is removed from the control. This is where I registered to receive the Focused event. Now it’s a simple matter that whenever the control gains focus my animation code in OnItemFocused will be called.

private async void OnItemFocused(object sender, FocusEventArgs e)
{
    await this.AssociatedObject.ScaleTo(Scale, 250, _easingFunction);
    await this.AssociatedObject.ScaleTo(1.00, 250, _easingFunction);
}

The animation is very straight forward. I use the ScaleTo method on View to scale the control up, then I call ScaleTo a second time to return it to its original size. If I didn’t want to provide any flexibility with my behavior, I could stop there. The rest of the code is just there to let me pass in parameters and configure how to perform the scale. Let’s look at Scale.

public static readonly BindableProperty ScaleProperty = BindableProperty.Create<AnimateSizeBehavior, double>(
    p => p.Scale,
    1.25,
    propertyChanged: OnScaleChanged);
 
public double Scale
{
    get { return (double)GetValue(ScaleProperty); }
    set { SetValue(ScaleProperty, value); }
}

First I set up a static BindableProperty called ScaleProperty. This is what lets me bind to properties on the behavior. The first parameter ties it to the double Scale instance property. The second parameter sets the default value to 1.25.

The EasingFunction property is a little more complicated. It requires validation when it is set. This is accomplished by setting the propertyChanged parameter in the factory method.

public static readonly BindableProperty EasingFunctionProperty = BindableProperty.Create<AnimateSizeBehavior, string>(
    p => p.EasingFunctionName,
    "SinIn",
    propertyChanged: OnEasingFunctionChanged);
 
private Easing _easingFunction;
 
public string EasingFunctionName
{
    get { return (string)GetValue(EasingFunctionProperty); }
    set { SetValue(EasingFunctionProperty, value); }
}
 
private static void OnEasingFunctionChanged(BindableObject bindable, string oldvalue, string newvalue)
{
    (bindable as AnimateSizeBehavior).EasingFunctionName = newvalue;
    (bindable as AnimateSizeBehavior)._easingFunction = GetEasing(newvalue);
}

The propertyChagned parameter is set to the static OnEasingFunctionChanged method. The instance of the behavior is passed in as the bindable parameter, along with the old and new values being set. In my example, we ignore the old value and just set the new value to the string property of EasingFunctionName. We also parse the new value to determine what type of easing function to use in the GetEasing method. If the easing function supplied is not an expected value, an exception is thrown. This happens not when we try to run the animation, but as soon as we set the value.

Now all I need to do is add the behavior to my text boxes. I’ll do this for the login page because there are two text boxes on the page so we can see how to tweak it.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:bLocal="clr-namespace:ShoppingCart.Behaviors;assembly=ShoppingCart"
             xmlns:b="clr-namespace:Xamarin.Behaviors;assembly=Xamarin.Behaviors"
             x:Class="ShoppingCart.Views.LoginPage" >
  <StackLayout VerticalOptions="FillAndExpand" Padding="50">
 
    <Entry Text ="{Binding Username}" Placeholder ="User name goes here" >
      <b:Interaction.Behaviors>
        <b:BehaviorCollection>
          <bLocal:AnimateSizeBehavior />
        </b:BehaviorCollection>
      </b:Interaction.Behaviors>
    </Entry>
 
    <Entry Text ="{Binding Password}"
      Placeholder ="Password goes here"
      HorizontalOptions="FillAndExpand">
      <b:Interaction.Behaviors>
        <b:BehaviorCollection>
          <bLocal:AnimateSizeBehavior EasingFunction="BounceIn"
                                      Scale="1.50" />
        </b:BehaviorCollection>
      </b:Interaction.Behaviors>
    </Entry>
 
  </StackLayout>
</ContentPage>

I stripped a lot out of the xaml here for clarity (like the submit button). The username textbox has the default behavior set. The password box changes the scale size to 1.5 and selects a different easing function. It would be possible to bind to those values as well, but it didn’t make sense in my already pointless example.

And that’s all there is to it. A quick thanks to lobrien for a useful sample on how to do animations in XF. This sped up my coding quite a bit. And of course a thanks to Corrado for the Xamarin.Forms.Behaviors library.

Happy Coding

this post was originally on the MasterDevs Blog

Comments