Cool Breeze
Cool Breeze

Reputation: 1369

How to reuse Interaction Behavior in XAML for WinRT app with an MVVM architecture

To bind the "loaded" and "unloaded" events I am using the following code in my XAML page:

<Page ...>
<interactivity:Interaction.Behaviors>
    <core:EventTriggerBehavior EventName="Loaded">
        <core:InvokeCommandAction Command="{Binding LoadedCommand}" />
    </core:EventTriggerBehavior>
    <core:EventTriggerBehavior EventName="Unloaded">
        <core:InvokeCommandAction Command="{Binding UnloadedCommand}" />
    </core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Grid />
</Page>

Everything works as expected however I am copying this same bit of code into every view? How can I go about making this reusable?

EDIT

I created an attached property using the code in this post.

My Attached property looks like this:

public static class UiBehaviors
{
    public static readonly DependencyProperty AttachedTriggersProperty = DependencyProperty.RegisterAttached("AttachedTriggers", typeof(EventTriggerCollection), typeof(UiBehaviors), new PropertyMetadata(null, OnAttachedTriggersChanged));

    private static void OnAttachedTriggersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        BehaviorCollection triggers = Interaction.GetBehaviors(d);

        if (e.OldValue != null)
        {
            foreach (EventTriggerBehavior trigger in (EventTriggerCollection)e.OldValue)
                triggers.Remove(trigger);
        }

        if (e.NewValue == null)
            return;

        foreach (EventTriggerBehavior trigger in (EventTriggerCollection)e.NewValue)
            triggers.Add(trigger);

    }

    public static void SetAttachedTriggers(DependencyObject element, EventTriggerCollection value)
    {
        element.SetValue(AttachedTriggersProperty, value);
    }

    public static EventTriggerCollection GetAttachedTriggers(DependencyObject element)
    {
        return (EventTriggerCollection)element.GetValue(AttachedTriggersProperty);
    }
}

public class EventTriggerCollection : Collection<EventTriggerBehavior>
{
}

My Xaml looks like this:

<Style x:Name="Test" TargetType="UserControl">
    <Setter Property="storeApplication:UiBehaviors.AttachedTriggers">
        <Setter.Value>
            <storeApplication:EventTriggerCollection>
                <core:EventTriggerBehavior EventName="Loaded">
                    <core:InvokeCommandAction Command="{Binding LoadedCommand}"  />
                </core:EventTriggerBehavior>
                <core:EventTriggerBehavior EventName="Unloaded">
                    <core:InvokeCommandAction Command="{Binding UnloadedCommand}" />
                </core:EventTriggerBehavior>
            </storeApplication:EventTriggerCollection>
        </Setter.Value>
    </Setter>
</Style>

The x:shared=False attribute is required on the EventTriggerCollection to create a new set of triggers each time the property is accessed. Without it a trigger will only work for the first control that accesses the property.

Unfortunately, I am unable to use this attribute because it is not supported in WinRT. See this post. I am now stuck :( What am I missing?

Upvotes: 0

Views: 212

Answers (1)

Filip Skakun
Filip Skakun

Reputation: 31724

You could subclass your page or define an attached property that does the same in a single attribute.

E.g. for the base class solution:

MainPage.xaml

<local:MyAppPageBase
    x:Class="App16.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App16"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    LoadedCommand="{Binding LoadedCommand}"
    UnloadedCommand="{Binding UnloadedCommand}">
    <Grid />
</local:MyAppPageBase>

MainPage.xaml.cs

using System.Windows.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App16
{
    public abstract class MyAppPageBase : Page
    {
        #region LoadedCommand
        /// <summary>
        /// LoadedCommand Dependency Property
        /// </summary>
        private static readonly DependencyProperty _LoadedCommandProperty =
            DependencyProperty.Register(
                "LoadedCommand",
                typeof(ICommand),
                typeof(MyAppPageBase),
                new PropertyMetadata(null));

        /// <summary>
        /// Identifies the LoadedCommand dependency property.
        /// </summary>
        public static DependencyProperty LoadedCommandProperty { get { return _LoadedCommandProperty; } }

        /// <summary>
        /// Gets or sets the LoadedCommand property. This dependency property 
        /// indicates the command to execute when the page loads.
        /// </summary>
        public ICommand LoadedCommand
        {
            get { return (ICommand)GetValue(LoadedCommandProperty); }
            set { this.SetValue(LoadedCommandProperty, value); }
        }
        #endregion

        #region UnloadedCommand
        /// <summary>
        /// UnloadedCommand Dependency Property
        /// </summary>
        private static readonly DependencyProperty _UnloadedCommandProperty =
            DependencyProperty.Register(
                "UnloadedCommand",
                typeof(ICommand),
                typeof(MyAppPageBase),
                new PropertyMetadata(null));

        /// <summary>
        /// Identifies the UnloadedCommand dependency property.
        /// </summary>
        public static DependencyProperty UnloadedCommandProperty { get { return _UnloadedCommandProperty; } }

        /// <summary>
        /// Gets or sets the UnloadedCommand property. This dependency property 
        /// indicates the command to execute when the page unloads.
        /// </summary>
        public ICommand UnloadedCommand
        {
            get { return (ICommand)GetValue(UnloadedCommandProperty); }
            set { this.SetValue(UnloadedCommandProperty, value); }
        }
        #endregion

        public MyAppPageBase()
        {
            this.Loaded += (s, e) =>
            {
                if (LoadedCommand?.CanExecute(null) == true)
                {
                    LoadedCommand.Execute(null);
                }
            };

            this.Unloaded += (s, e) =>
            {
                if (UnloadedCommand?.CanExecute(null) == true)
                {
                    UnloadedCommand.Execute(null);
                }
            };
        }
    }

    public sealed partial class MainPage : MyAppPageBase
    {
        public MainPage()
        {
            this.InitializeComponent();
        }
    }
}

Attached property (attached behavior) solution is a bit more involved as an attached property needs to make sure it doesn't lead to leaks caused by event subscriptions on a static class, but has the benefit of not requiring changing your base classes, which is especially useful if you want to reuse it in more than one project, perhaps by putting it in a NuGet package:

MainPage.xaml

<Page
    x:Class="App16.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App16"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    local:ElementExtensions.LoadedCommand="{Binding LoadedCommand}">
    <Grid />
</Page>

MainPage.xaml.cs

using System;
using System.Diagnostics;
using System.Windows.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App16
{
    public static class ElementExtensions
    {
        #region LoadedCommand
        /// <summary>
        /// LoadedCommand Attached Dependency Property
        /// </summary>
        private static readonly DependencyProperty _LoadedCommandProperty =
            DependencyProperty.RegisterAttached(
                "LoadedCommand",
                typeof(ICommand),
                typeof(ElementExtensions),
                new PropertyMetadata(null, OnLoadedCommandChanged));

        /// <summary>
        /// Identifies the LoadedCommand dependency property.
        /// </summary>
        public static DependencyProperty LoadedCommandProperty { get { return _LoadedCommandProperty; } }

        /// <summary>
        /// Gets the LoadedCommand property. This dependency property 
        /// indicates the command to execute when the element loads.
        /// </summary>
        public static ICommand GetLoadedCommand(DependencyObject d)
        {
            return (ICommand)d.GetValue(LoadedCommandProperty);
        }

        /// <summary>
        /// Sets the LoadedCommand property. This dependency property 
        /// indicates the command to execute when the element loads.
        /// </summary>
        public static void SetLoadedCommand(DependencyObject d, ICommand value)
        {
            d.SetValue(LoadedCommandProperty, value);
        }

        /// <summary>
        /// Handles changes to the LoadedCommand property.
        /// </summary>
        /// <param name="d">
        /// The <see cref="DependencyObject"/> on which
        /// the property has changed value.
        /// </param>
        /// <param name="e">
        /// Event data that is issued by any event that
        /// tracks changes to the effective value of this property.
        /// </param>
        private static void OnLoadedCommandChanged(
            DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ICommand oldLoadedCommand = (ICommand)e.OldValue;
            ICommand newLoadedCommand = (ICommand)d.GetValue(LoadedCommandProperty);

            if (oldLoadedCommand != null)
            {
                var handler = GetLoadedCommandHandler(d);
                handler?.Detach((FrameworkElement) d);
            }

            if (newLoadedCommand != null)
            {
                SetLoadedCommandHandler(d, new LoadedCommandHandler((FrameworkElement)d));
            }
        }
        #endregion

        #region LoadedCommandHandler
        /// <summary>
        /// LoadedCommandHandler Attached Dependency Property
        /// </summary>
        private static readonly DependencyProperty _LoadedCommandHandlerProperty =
            DependencyProperty.RegisterAttached(
                "LoadedCommandHandler",
                typeof(LoadedCommandHandler),
                typeof(ElementExtensions),
                new PropertyMetadata(null));

        /// <summary>
        /// Identifies the LoadedCommandHandler dependency property.
        /// </summary>
        public static DependencyProperty LoadedCommandHandlerProperty { get { return _LoadedCommandHandlerProperty; } }

        /// <summary>
        /// Gets the LoadedCommandHandler property. This dependency property 
        /// indicates the object that handles Loaded events on its owning element.
        /// </summary>
        internal static LoadedCommandHandler GetLoadedCommandHandler(DependencyObject d)
        {
            return (LoadedCommandHandler)d.GetValue(LoadedCommandHandlerProperty);
        }

        /// <summary>
        /// Sets the LoadedCommandHandler property. This dependency property 
        /// indicates the object that handles Loaded events on its owning element.
        /// </summary>
        internal static void SetLoadedCommandHandler(DependencyObject d, LoadedCommandHandler value)
        {
            d.SetValue(LoadedCommandHandlerProperty, value);
        }
        #endregion

        internal class LoadedCommandHandler
        {
            public LoadedCommandHandler(FrameworkElement element)
            {
                element.Loaded += OnLoaded;
            }

            public void Detach(FrameworkElement element)
            {
                element.Loaded -= OnLoaded;
            }

            private void OnLoaded(object sender, RoutedEventArgs e)
            {
                var command = GetLoadedCommand((DependencyObject) sender);
                if (command?.CanExecute(null) == true)
                {
                    command.Execute(null);
                }
            }
        }
    }

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            this.DataContext = new MyViewModel();
        }
    }

    public class MyViewModel
    {
        public ICommand LoadedCommand { get; private set; } = new MyCommand();
    }

    public class MyCommand : ICommand
    {
        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            Debug.WriteLine("Blahr");
        }

        public event EventHandler CanExecuteChanged;
    }
}

Upvotes: 2

Related Questions