microsoft/XamlBehaviorsWpf

Make TriggerCollection Constructor Public

josephdrake-stahls opened this issue · 1 comments

Is your feature request related to a problem? Please describe.
I was looking at a use case where I wanted multiple triggers to fire the same action. I could have repeated the action for every trigger, but it smells bad. I had a hard time writing a solution because the access modifiers for various structures are limited. My solution was a "MultiTrigger" implementation that utilizes a TriggerCollection, but due to its accessibility, I had to had it together using the Interaction.GetTriggers method (see additional context).

Describe the solution you'd like
TriggerCollection is general enough that it could have a public constructor.
https://github.com/microsoft/XamlBehaviorsWpf/blob/master/src/Microsoft.Xaml.Behaviors/TriggerCollection.cs#L17

The only usage of the class is in Interaction and that is locked down to prevent a TriggerCollection from overwriting an existing TriggerCollection on the TriggersProperty.
https://github.com/microsoft/XamlBehaviorsWpf/blob/master/src/Microsoft.Xaml.Behaviors/Interaction.cs#L36
https://github.com/microsoft/XamlBehaviorsWpf/blob/master/src/Microsoft.Xaml.Behaviors/Interaction.cs#L62

While its understandable that future changes could break the TriggerCollection and leaving it as internal could prevent a breaking change, I think this risk is warranted.

Describe alternatives you've considered

  • Writing my own TriggerCollection implementation. It would essentially amount to a copy paste of existing code and thereby smells bad.
  • Having XamlBehaviorsWpf implement a MultiTrigger (this may be preferable to control breaking changes)

Additional context
Here is my MultiTrigger implementation. I have my reasons for using Return/Tab instead of a DataTrigger and I believe that conversation is moot to the general problem of a multitrigger.
XAML:

        xmlns:CoreBehaviors="clr-namespace:MyCode.Core.Behaviors"
...
                <TextBox 
                    x:Name="tbLookup">
                    <b:Interaction.Triggers>
                        <CoreBehaviors:MultiTrigger>
                            <CoreBehaviors:MultiTrigger.Triggers>
                                <b:KeyTrigger Key="Return" />
                                <b:KeyTrigger Key="Tab" />
                            </CoreBehaviors:MultiTrigger.Triggers>
                            
                            <b:InvokeCommandAction Command="{Binding LookupCommand}" CommandParameter="{Binding Text, ElementName=tbLookup}" />
                        </CoreBehaviors:MultiTrigger>
                    </b:Interaction.Triggers>
                </TextBox>

MultiTrigger.cs:

using Microsoft.Xaml.Behaviors;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using TriggerBase = Microsoft.Xaml.Behaviors.TriggerBase;

namespace MyCode.Core.Behaviors
{
    public class MultiTrigger : TriggerBase<DependencyObject>
    {
        public MultiTrigger()
        {
            Triggers = Interaction.GetTriggers(this); //HACK: use GetTriggers to "construct" TriggerCollection
            Triggers.Detach();  //HACK: We need to immediately detach the associated object (this) so we can attach it to this instances associated object in OnAttached
            ((INotifyCollectionChanged)Triggers).CollectionChanged += MultiTrigger_CollectionChanged;
        }


        public Microsoft.Xaml.Behaviors.TriggerCollection Triggers { get; }

        private void MultiTrigger_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.OldItems != null)
                foreach (TriggerBase item in e.OldItems.Cast<TriggerBase>())
                    foreach(MultiTriggerActionAdapter? action in item.Actions.OfType<MultiTriggerActionAdapter>())
                        item.Actions.Remove(action);

            if (e.NewItems != null)
                foreach (TriggerBase item in e.NewItems.Cast<TriggerBase>())
                    item.Actions.Add(new MultiTriggerActionAdapter(this));
        }

        protected override void OnAttached()
        {
            Triggers.Attach(AssociatedObject);
            base.OnAttached();
        }

        protected override void OnDetaching()
        {
            Triggers.Detach();
            base.OnDetaching();
        }

        private class MultiTriggerActionAdapter : TriggerAction<DependencyObject>
        {
            private readonly MultiTrigger _parent;

            public MultiTriggerActionAdapter(MultiTrigger parent)
            {
                _parent = parent;
            }
            protected override void Invoke(object parameter)
                => _parent.InvokeActions(parameter);
        }
    }
}

I have no problem with making the ctor of the TriggerCollection public. Since no one else from MSFT has denied this request, go ahead and submit your PR and we'll get it in the product.