microsoft / XamlBehaviorsWpf

Home for WPF XAML Behaviors on GitHub.
MIT License
854 stars 140 forks source link

Make TriggerCollection Constructor Public #103

Closed josephdrake-stahls closed 1 year ago

josephdrake-stahls commented 2 years ago

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

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);
        }
    }
}
brianlagunas commented 1 year ago

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.