4

I have a TargetedTriggerAction from a 3rd party library that would like to call/invoke without attaching it to a button. I have no problem getting it to work with the button, but I want to do it in response to some non-UI event.

Here is the action's class declaration:

 public class MeasureAction : TargetedTriggerAction<Map>

Here is my setup code so far:

    var measure = new MeasureAction();
    measure.TargetObject = _mapControl;
    measure.MeasureMode = MeasureAction.Mode.Polyline;
    measure.MapUnits = DistanceUnit.Miles;

I want to be able to do something like this, but I know Invoke is protected:

measure.Invoke();
Keith G
  • 4,580
  • 5
  • 38
  • 48

3 Answers3

8

To invoke your trigger action, you need a trigger!

namespace TriggerTest
{
    using System.Windows;

    /// <summary>
    /// A trigger that may be invoked from code.
    /// </summary>
    public class ManualTrigger : System.Windows.Interactivity.TriggerBase<DependencyObject>
    {
        /// <summary>
        /// Invokes the trigger's actions.
        /// </summary>
        /// <param name="parameter">The parameter value.</param>
        public void Invoke(object parameter)
        {
            this.InvokeActions(parameter);
        }
    }
}

The above is a trigger implementation that may be invoked without any UI dependencies. For example:

var measure = new MeasureAction();
measure.TargetObject = _mapControl;
measure.MeasureMode = MeasureAction.Mode.Polyline;
measure.MapUnits = DistanceUnit.Miles; 

ManualTrigger trigger = new ManualTrigger();
trigger.Actions.Add(measure);
trigger.Invoke(null);

To make calling this even easier, you could add an extension method to TriggerAction.

namespace TriggerTest
{
    using System.Windows.Interactivity;

    /// <summary>
    /// Allows a trigger action to be invoked from code.
    /// </summary>
    public static class TriggerActionExtensions
    {
        /// <summary>
        /// Invokes a <see cref="TriggerAction"/> with the specified parameter.
        /// </summary>
        /// <param name="action">The <see cref="TriggerAction"/>.</param>
        /// <param name="parameter">The parameter value.</param>
        public static void Invoke(this TriggerAction action, object parameter)
        {
            ManualTrigger trigger = new ManualTrigger();
            trigger.Actions.Add(action);

            try
            {
                trigger.Invoke(parameter);
            }
            finally
            {
                trigger.Actions.Remove(action);
            }
        }

        /// <summary>
        /// Invokes a <see cref="TriggerAction"/>.
        /// </summary>
        /// <param name="action">The <see cref="TriggerAction"/>.</param>
        public static void Invoke(this TriggerAction action)
        {
            action.Invoke(null);
        }
    }
}

Now you can write what you really wanted:

var measure = new MeasureAction();
measure.TargetObject = _mapControl;
measure.MeasureMode = MeasureAction.Mode.Polyline;
measure.MapUnits = DistanceUnit.Miles; 
measure.Invoke();
Olly
  • 5,966
  • 31
  • 60
  • Fantastic answer, and this is essentially what I was hoping for, but it's not working. I'm getting an exception: "Object reference not set to an instance of an object". Looking at the details, it looks like there may be a private variable inside the MeasureAction class that maybe is not being initialized. Can't tell for sure, though. I can see the whole MeasureAction class in reflector. Is there something I might look for to better diagnose? – Keith G Oct 19 '12 at 21:11
  • Clutching at straws here, but is there any code in `MeasureAction` that depends on the `AssociatedObject`? Maybe you could just set that to `_mapControl` too for good measure. – Olly Oct 20 '12 at 01:56
  • 1
    Since you can see the internals in the debugger, you could try using `MeasureAction` in the conventional way in XAML and examine its properties. Maybe create a button with a click handler and set a breakpoint there. Then compare it to your `measure` object just before you call `Invoke()`. What's different? – Olly Oct 20 '12 at 01:59
  • Interestingly, I am getting the same error when I invoke using the conventional method in XAML! I'll track that down and follow-up. – Keith G Oct 22 '12 at 15:16
0

Is the 3rd party class sealed?

If not, the "best" answer would probably be to derive your own class from MeasureAction and add a invoker (keep it internal/protected if you want to keep things clean).

If it's sealed/otherwise frozen, your best bet is probably Reflection - there are ways to cache/otherwise speed up the cost of this call, but the basic code would be something like:

// Paste in the code in the original question here, name of instance == measure
var actionMethod = typeof(MeasureAction)
    .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
    .FirstOrDefault(meth => meth.Name == "Invoke");
if(actionMethod != null)
{
    var result = actionMethod.Invoke(measure, null);
}
JerKimball
  • 16,584
  • 3
  • 43
  • 55
  • It's not sealed, but it is internal to the 3rd-party library. Do you have some example code on how to add an invoker to a derived class? – Keith G Oct 16 '12 at 20:28
  • Ah, if it's "properly" internal (i.e., is marked as an internal class), it's just as good as sealed...there's no clean way to derive from an internal class that I'm aware of. You might be stuck with the reflection route. If that is the case, there are a number of ways you can make sure the hit from Reflection is a one-time hit; Expression trees, Delegate.CreateDelegate, caching the methodinfo returned, etc. – JerKimball Oct 16 '12 at 21:12
  • Thanks for the replies so far. If I run it as you have listed, I get a TargetParameterCountException - Parameter count mismatch. So I added a parameter using this syntax: object []param = new object[1]; param[0] = _mapControl; var result = actionMethod.Invoke(measure, param); But I still get an exception with "Exception has been thrown by the target of an invocation" and (inner exception) "Object reference not set to an instance of an object". None of the objects (measure, param, param[0], _mapControl) are null. – Keith G Oct 17 '12 at 16:13
  • Sorry, didn't notice this response earlier - if you're getting a NullRef, likely some "setup" step hasn't been done prior to invocation - without the actual code/disassembled source, there's little I can offer in way of assistance...I would recommend throwing the third party library into Reflector and seeing what it's trying to do. – JerKimball Nov 14 '12 at 18:58
0

Old post but may help. I did like this

public class MyMeasureAction : MeasureAction
{
    public void Execute()
    {
        OnTargetChanged(null, (Map)TargetObject);
        Invoke(null);
    }
}

...

var mymeasure = new MyMeasureAction();
mymeasure.TargetObject = MyMap;
mymeasure.xxxx = xxxx
....
mymeasure.Execute();

The OnTargetChanged() method will initialize the internal Map of the MeasureAction

hugo
  • 1,829
  • 1
  • 18
  • 47