Damian Mehers' Blog Xamarin from Geneva, Switzerland.

27Sep/160

Using Styles and Data Triggers to disable Xamarin forms while waiting

It's a common scenario: You are sending data to a service, or waiting for something to happen, and you don't want the user to interact with your form while that is happening.

untitled-1

The naive approach is to bind the IsEnabled property on your containing Layout to a boolean property in your View Model, but you'll soon find that IsEnabled is not inherited. Setting it on a StackLayout doesn't set it on all the controls embedded within that layout.

Here is a solution which binds the IsRunning property of an ActivityIndicator to a View Model property, and then uses a Style and a DataTrigger to react to the ActivityIndicator's running by setting the IsEnabled properties on the Layouts contained controls:

First I overlay an ActivityIndicator over my form using an AbsoluteLayout:

  <AbsoluteLayout>
    <ActivityIndicator
        IsRunning="{Binding Loading}" HorizontalOptions="Center" VerticalOptions="Center"
        IsVisible="{Binding Loading}" AbsoluteLayout.LayoutBounds="0,0,1,1"
        AbsoluteLayout.LayoutFlags="All" x:Name="ActivityIndicator" />
    <StackLayout Orientation="Vertical"
                 AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All">

Next in my StackLayout I define an explicit Style with a DataTrigger which disables the targeted control and sets its Opacity to 30% when the ActivityIndicator is running:

      <StackLayout.Resources>
        <ResourceDictionary>
          <Style TargetType="View" x:Key="MyBase">
            <!-- Disable controls when the activity indicator is running -->
            <Style.Triggers>
              <DataTrigger
                  TargetType="View"
                  Binding="{Binding Source={x:Reference ActivityIndicator}, Path=IsRunning}"
                  Value="True">
                <Setter Property="Opacity" Value="0.3" />
                <Setter Property="IsEnabled" Value="False" />
              </DataTrigger>
            </Style.Triggers>
          </Style>

You might hope that using an implicit style instead of an explicit style above would affect all views contained within the StackLayout, but it doesn't work like that. There is also the tantalizing and undocumented ApplyToDerivedTypes Style property, but that has no impact that I am aware of.

So instead I create implicit styles for each specific type of control I use inside my StackPanel:

          <!-- Define implicit styles for each control we use. -->
          <Style TargetType="Label" BasedOn="{StaticResource MyBase}" />
          <Style TargetType="Entry" BasedOn="{StaticResource MyBase}" />
          <Style TargetType="Button" BasedOn="{StaticResource MyBase}" />
        </ResourceDictionary>
      </StackLayout.Resources>

At least I'm able to reuse my base style. So here is my final complete view (but I'm not quite done yet):

<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:shared="clr-namespace:LoadingDemo.Shared;assembly=LoadingDemo.Shared"
             BindingContext="{x:Static shared:Locator.MyViewModel}"
             x:Class="LoadingDemo.MainPage">

  <!-- Use an absolute layout to overlay one control over another -->
  <AbsoluteLayout>
    <ActivityIndicator
        IsRunning="{Binding Loading}" HorizontalOptions="Center" VerticalOptions="Center"
        IsVisible="{Binding Loading}" AbsoluteLayout.LayoutBounds="0,0,1,1"
        AbsoluteLayout.LayoutFlags="All" x:Name="ActivityIndicator" />

    <StackLayout Orientation="Vertical"
                 AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All">

      <StackLayout.Resources>
        <ResourceDictionary>
          <Style TargetType="View" x:Key="MyBase">
            <!-- Disable controls when the activity indicator is running -->
            <Style.Triggers>
              <DataTrigger
                  TargetType="View"
                  Binding="{Binding Source={x:Reference ActivityIndicator}, Path=IsRunning}"
                  Value="True">
                <Setter Property="Opacity" Value="0.3" />
                <Setter Property="IsEnabled" Value="False" />
              </DataTrigger>
            </Style.Triggers>
          </Style>

          <!-- Define implicit styles for each control we use. -->
          <Style TargetType="Label" BasedOn="{StaticResource MyBase}" />
          <Style TargetType="Entry" BasedOn="{StaticResource MyBase}" />
          <Style TargetType="Button" BasedOn="{StaticResource MyBase}" />
        </ResourceDictionary>
      </StackLayout.Resources>

      <Label Text="My Label" HorizontalOptions="Center" />
      <Entry Placeholder="Enter text here" />
      <Entry Placeholder="Enter text here" />
      <Entry Placeholder="Enter text here" />
      <Button Text="Click Me" Command="{Binding StartCommand}" HorizontalOptions="Center" />
    </StackLayout>
  </AbsoluteLayout>
</ContentPage>

This is my View Model:

  public class ViewModel : INotifyPropertyChanged {
    private bool _loading;

    public ViewModel() {
      StartCommand = new Command(Start);
    }

    private async void Start() {
      Loading = true;
      await Task.Delay(TimeSpan.FromSeconds(5));
      Loading = false;
    }

    public bool Loading {
      get {
        return _loading;
      }
      private set {
        _loading = value;
        OnPropertyChanged();
      }
    }

    public ICommand StartCommand { get; }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }

When I test the above code in an app I created, everything works swimmingly except for the Button. It doesn't become disabled when I click it.

The reason is that I am using the Command property to execute code in my View Model, and the ICommand interface to which it is bound has its very own CanExecute mechanism to decide when the Button can be clicked.

The solution is in the View Model, and involves raising the CanExecuteChanged event:

    public ViewModel() {
      // When you click the button run the Start method.  The command is available
      // when not loading
      StartCommand = new Command(Start, canExecute: () => !Loading);
    }
...
public bool Loading {
      get { ... }
      private set {
        _loading = value;
        OnPropertyChanged();
        StartCommand.ChangeCanExecute();
      }
    }

Here is the final View Model:

  public class ViewModel : INotifyPropertyChanged {
    private bool _loading;

    public ViewModel() {
      // When you click the button run the Start method.  The command is available
      // when not loading
      StartCommand = new Command(Start, () => !Loading);
    }

    private async void Start() {
      Loading = true;
      await Task.Delay(TimeSpan.FromSeconds(5));
      Loading = false;
    }

    public bool Loading {
      get {
        Debug.WriteLine($"Returning {_loading}");
        return _loading;
      }
      private set {
        _loading = value;
        OnPropertyChanged();
        StartCommand.ChangeCanExecute();
      }
    }

    public Command StartCommand { get; }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }

I like to put my View Models in a separate class library, which has no dependencies on Xamarin Forms, but the Command class comes from Xamarin Forms. Fortunately Xamarin Forms is now open source, so I can "borrow" the Command class' source and embed it within my class library and thus remove the Xamarin Forms dependency.

The complete solution is here in GitHub.

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.