02 July 2013

Behavior to hide UI elements when a bound collection is empty

screenshot1This is a fun little thingy I wrote when dealing with error lists. Suppose, you want the user to be able to see there are errors, but not directly fly them the whole detailed error list in his face. So there is, for instance a button “Show errors”, like in the application showed to the right.

But, it’s ugly “Show errors” is always visible, even if there are no errors to display. I actually only want to have this button appear when there are errors indeed, so that it acts as an error indicator, and then the user can decide if she wants to see them or not. Of course you can fix that in your viewmodel – subscribe to the events of an ObservableCollection of errors, and turn visibility on or off when trapping those events, every time you need to do this. Perfectly viable solution. But an even better solution is to encapsulate that behavior – the word just says it – in a simple piece of reusable code.

Meet HideWhenCollectionEmptyBehavior. It sports an INotifyCollectionChanged Collection to which you can bind, and the rest of it is actually so simple I am going to show it in one go:

using System.Collections;
using System.Collections.Specialized;
using System.Windows;

namespace Wp7nl.Behaviors
{
  public class HideWhenCollectionEmptyBehavior : SafeBehavior<FrameworkElement>
  {
    protected override void OnSetup()
    {
      base.OnSetup();
      SetVisibility();
    }

    protected override void OnCleanup()
    {
      base.OnCleanup();
      if (Collection != null)
      {
        Collection.CollectionChanged -= OnCollectionChanged;
      }
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      SetVisibility();
    }

    private void SetVisibility()
    {
      var collection = Collection as ICollection;
      AssociatedObject.Visibility = 
        collection != null && collection.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
    }
  }
 }

The behavior is implemented as a SafeBehavior with an attached dependency property “Collection” of type INotifyPropertyChanged to which you can bind the collection that needs to be monitored. The core of the whole behavior is simply the SetVisibility method, which casts the collection to ICollection and checks if it’s null or empty – in that case the Visibility of the object to which this behavior is attached is set to Collapsed – if not, it’s set to Visible.

The attached dependency property is fairly standard, apart from the last part:

#region Collection

public const string CollectionPropertyName = "Collection";

public INotifyCollectionChanged Collection
{
  get { return (INotifyCollectionChanged)GetValue(CollectionProperty); }
  set { SetValue(CollectionProperty, value); }
}

public static readonly DependencyProperty CollectionProperty = DependencyProperty.Register(
    CollectionPropertyName,
    typeof(INotifyCollectionChanged),
    typeof(HideWhenCollectionEmptyBehavior),
    new PropertyMetadata(default(INotifyCollectionChanged), OnCollectionChanged));

public static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var behavior = d as HideWhenCollectionEmptyBehavior;
  var newValue = (INotifyCollectionChanged)e.NewValue;
  var oldValue = (INotifyCollectionChanged)e.OldValue;
  if (behavior != null)
  {
    if (oldValue != null)
    {
      oldValue.CollectionChanged -= behavior.OnCollectionChanged;
    }
    if (newValue != null)
    {
      newValue.CollectionChanged += behavior.OnCollectionChanged;
    }

    behavior.SetVisibility();
  }
}

#endregion

There’s a lot of plumbing going on ‘just to be on the safe side’ – if the bound collection is replaced, the CollectionChanged event is detached from the old collection and attached to the new collection. Everything to prevent memory leaks:-) - but in reality I think only the newValue will ever be set.But anyway, by making the property of type INotifyCollectionChanged, I am sure to have the lowest common denominator and still have a CollectionChanged that I can trap.

By dragging the behavior on top of the “Show Errors”  button and binding to the collections of errors, my app initially looks like displayed on the left. Only when I click “add errors” the “show errors” button appears (courtesy of HideWhenCollectionEmptyBehavior) and then when I click it, I get to see the actual errors.

screenshot3

screenshot1

screenshot2

 

 

 

 

 

 

 

Now of course this is a pretty contrived example, but it is a real-word use case. Like I wrote, I actually use it for indicating that there are errors, but I can think of a lot of other scenarios, for instance an UI element that is displayed when an object has child objects (say, an order has order lines) without actually displaying them – only indicating they are present.

I wrote this behavior to act in a Windows Phone application but the code is so generic it will work in other XAML platforms as well, including Windows 8.

The code of the behavior, together with my beautiful *cough* sample app can be found here

For those who’d like me to use “Any()” in stead of “Count > 0 “ I would like to point out that ICollection is just ICollection, not ICollection<T> and that does not seem to support “Any()”

5 comments:

Yorkshire Hills Board said...

I often will attach an IsEmpty property. Then

Visibility={Binding MyCollection.IsEmpty, Converter=BooleanToVisibilityConverter}

I've found "testing for empty-ness" is handy in many situations in XAML.

enough said...

Hi Joost,

thanks for the code, but I wonder if a simple Converter is not enough for this usecase?

Sample C# code:

public class CollectionNotEmptyToVisibiliyConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
bool isEmtpyOrNull = (value == null || !(value is ICollection) || ((ICollection)value).Count == 0);
if ("reverse".Equals(parameter))
{
isEmtpyOrNull = !isEmtpyOrNull;
}
return isEmtpyOrNull ? Visibility.Collapsed : Visibility.Visible;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}


Sample XAML code:

<Button Content="Show Errors" Command="{Binding DisplayErrors, Mode=OneWay}" Margin="0,59,0,0" Visibility="{Binding Errors, Converter={StaticResource CollectionNotEmptyToVisibiliyConverter}}"/>

Where's the benefit for a behavior?

Thanks,
Robert

Joost van Schaik said...

@Robert, I tried the converter. I could not get that to work on the following scenario:
1) Empty collection
2) Bind
3) UI element is invisible
4) Add something to collection
5) UI element is still invisible

The converter is only called when the property that holds the collection is changed. Not when I add or delete objects to that collection

Joost van Schaik said...

@Yorkshire Hill Board Indeed. But then you have to implement that in every view model where you want to use it AND trap the CollectionChanged event in that viewmodel, manually setting the IsEmpty property. This is drag-drop and reuse. I am a lazy ******* ;-)

Jarno Peschier said...

Then again, if you work with a collection interface that has a O(1) runtime complexity Count property, why would you even consider going for a O(n) runtime complexity Any() extension method at all? Would only think of that when working with arbitrary sequences.