Filters Recipe & Coroutine.Execute

Mar 9, 2011 at 10:32 AM

I'm opening this thread in reply to @matthidinger, who have had some problems with Action Filters when using Coroutine.Execute invocation.

First of all, I have to apologize with Matt (and Rob...) for my short presence here in the forum lately.
Coming to the topic, after reviewing the filters code, I realized that I already added, some time ago, a method aimed to invoke an action honouring the filters decoration.

The method is FilterManager.ExecuteAction and you can find it in my CM fork: 

https://hg01.codeplex.com/forks/marcoamendola/caliburnmicromarcoamendolafork/

That method, however, is far from ideal because:
- has an horrible string parameter (which can easily be fixed using a Lambda Expression, though)
- is implemented with an hackish trick
- has to be invoked explicitly, thus coupling the VM to the filter infrastructure

I'm pretty sure it could, and should :-), be improved using the Coroutine.Execute extensibility.
My plan is to improve the syntax for calling "regular" void actions (which unfortunately cannot be intercepted without an explicit call) and plug the filters execution in Coroutine.Execute.

I'll post the result here as soon as I'm done.

Mar 9, 2011 at 3:48 PM
Edited Mar 9, 2011 at 3:54 PM

Thanks a ton for looking into this Marco. I was tweeting with Rob a bit yesterday on this topic, since I felt like I was abusing Coroutine.BeginExecute -- I use it in almost every OnActivated/OnViewLoaded to start getting the data for the view model. Unfortunately in doing so I was unable to use the [SetBusy] attributes, etc.

To get slightly off track regarding this thread --

I have only used CM for my WP7 apps so far, so I'm not entirely fluent in its capabilities/extensibility points. I wonder if it would be possible to add an virtual method to Screen (or a base class)

public class MyViewModel : Screen
{
     [SetBusy]
     public override IEnumerable<IResult> ActivationActions()
     {
           yield return LoadData();
     }
}

Caliburn would handle invoking this method during the viewmodel activation (or load, possibly... not sure best way to handle that, short of also add LoadActions()). It would setup the ActionExecutionContext with the correct View, etc

-Matt

Mar 10, 2011 at 5:18 PM
Edited Mar 20, 2011 at 9:45 AM

Hi Matt,

I played around Coroutine.BeginExecute and filters.
Unfortunately I found no way to access the filter attributes on the coroutine function *without* changing the signature of Coroutine.BeginExecute.
So I resorted to use a filter-specifice method to invoke both simple actions and coroutine.
I believe that it is an acceptable arrangement:

 

class MyViewModel : Screen {

	protected override void OnInitialize()
 	{
		base.OnInitialize();
		FilterManager.ExecuteAction(() => MyCoroutine());
	}
	
	[SomeFilterAttribute]
	public IEnumerable<IResult> MyCoroutine() {
		//code omitted
	}
}

 

FilterManager.ExecuteAction also works with plain (void) actions.
Using FilterManager.ExecuteAction you can build you base Screen with activation actions:

 

class ActivableScreen : Screen {

	protected override void OnInitialize()
 	{
		base.OnInitialize();
		FilterManager.ExecuteAction(() => ActivationActions());
	}
	
	[SetBusy]
	public virtual IEnumerable<IResult> ActivationActions() {
		yield break;
	}
}

class MyViewModel : ActivableScreen {

	public override IEnumerable<IResult> ActivationActions() {
		yield return LoadData();
	}
}

 

I'm posting the framework code here because the proxy I'm behind now don't let me push to HG. I'll do later.

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Caliburn.Micro;
using System.Linq.Expressions;

namespace Caliburn.Micro.Recipes.Filters.Framework
{
	public static class FilterManager
	{

		public static IResult WrapWith(this IResult inner, IEnumerable<IExecutionWrapper> wrappers)
		{
			IResult previous = inner;
			foreach (var wrapper in wrappers)
			{
				previous = wrapper.Wrap(previous);
			}
			return previous;
		}


		public static Func<ActionExecutionContext, IEnumerable<IFilter>> GetFiltersFor = (context) =>
		{
			//TODO: apply caching?

			var filters = context.Target.GetType().GetAttributes<IFilter>(true);

			if (context.Method != null)
				filters = filters.Union(context.Method.GetAttributes<IFilter>(true));

			filters.OrderBy(x => x.Priority);

			return filters;

		};


	 

		public static void ExecuteAction(Expression<System.Action> action)
		{
			ExecuteActionImpl(action);
		}
		public static void ExecuteAction(Expression<Func<IEnumerable<IResult>>> coroutine)
		{
			ExecuteActionImpl(coroutine);
		}
		public static void ExecuteAction(Expression<Func<IEnumerator<IResult>>> coroutine)
		{
			ExecuteActionImpl(coroutine);
		}

		static void ExecuteActionImpl(LambdaExpression lambda)
		{
			var call = lambda.Body as MethodCallExpression;
			if (call == null) throw new ArgumentException("Execute action only supports lambda in the form FilterManager.ExecuteAction(() => vm.MyAction()), being MyAction void, IEnumerable<IResult> or IEnumerator<IResult>");

			var targetExp = call.Object as ConstantExpression;
			if (targetExp == null) throw new ArgumentException("Execute action only supports lambda in the form FilterManager.ExecuteAction(() => vm.MyAction()), being 'vm' an object instance");

			var context = new ActionExecutionContext
			{
				Method = call.Method,
				Target = targetExp.Value
			};

			FilterFrameworkCoreCustomization.InvokeAction(context, new object[] { });
		}
		 

	}
 
	public static class FilterFrameworkCoreCustomization
	{
		public static void Hook()
		{
			var oldPrepareContext = ActionMessage.PrepareContext;
			ActionMessage.PrepareContext = context =>
			{
				oldPrepareContext(context);
				FilterFrameworkCoreCustomization.PrepareContext(context);
			};

			ActionMessage.InvokeAction = context=> {
				var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters());
				FilterFrameworkCoreCustomization.InvokeAction(context, values);
			};
		}

		internal static void PrepareContext(ActionExecutionContext context)
		{
			var contextAwareFilters = FilterManager.GetFiltersFor(context).OfType<IContextAware>()
				.ToArray();
			contextAwareFilters.Apply(x => x.MakeAwareOf(context));

			context.Message.Detaching += (o, e) =>
			{
				contextAwareFilters.Apply(x => x.Dispose());
			};
		}

		internal static void InvokeAction(ActionExecutionContext context, object[] values)
		{
			IResult result = Coroutine.CreateParentEnumerator(ExecuteActionWithParameters(values).GetEnumerator());
			var wrappers = FilterManager.GetFiltersFor(context).OfType<IExecutionWrapper>();
			var pipeline = result.WrapWith(wrappers);

			//if pipeline has error, action execution should throw! 
			pipeline.Completed += (o, e) =>
			{
				Execute.OnUIThread(() =>
				{
					if (e.Error != null) throw new Exception(
						string.Format("An error occurred while executing {0}", context.Message),
						e.Error
					);
				});
			};

			pipeline.Execute(context);
		}

		private static IEnumerable<IResult> ExecuteActionWithParameters(object[] values) {
			var actionExecution = new ExecuteActionResult(values);
			yield return actionExecution;

			var outcomeEnumerator = actionExecution.GetOutcomeEnumerator();
			if (outcomeEnumerator == null) yield break;

			try
			{
				while (outcomeEnumerator.MoveNext())
				{
					yield return outcomeEnumerator.Current;
				}
			}
			finally {
				outcomeEnumerator.Dispose();
			}

			
		}

		
	}

EDIT: fixed an issue in coroutine invocation. Thanks janoveh.

Mar 10, 2011 at 11:11 PM

Pushed here: https://hg01.codeplex.com/forks/marcoamendola/caliburnmicromarcoamendolafork

The solution is in \samples\Caliburn.Micro.Recipes.Filters

Mar 19, 2011 at 7:31 PM

Hi!

This version of the filter framework doesn't work, I'm afraid...

In your sample, decorate the ChattyDivide() coroutine with a SetBusy-attribute and observe that the attribute is never "called".

I think the problem is that InvokeAction will never be called when Coroutine.BeginExceute(...) is called already in ExecuteAction (last line).

 

Mar 20, 2011 at 9:39 AM

Thanks for pointing it out. It is fixed now.

Mar 20, 2011 at 11:59 AM

Hi!

It now works for parameterless coroutines. 

My suggestion is to temporarily store the original expression in the ActionExecutionContext. 

Then you need to modify the ExecuteActionResult to look for an expression and compile/invoke it, instead of invoking context.Method.

Parameters will then be properly handled, I think.  That way you will support more advanced expressions:

FilterManager.ExecuteAction(() => MyCoroutine(myClass1, myInt1 + myInt2));

 

Mar 20, 2011 at 3:19 PM

Hi!

Here is my proposal:

    public static class FilterManager
    {
        public static Func<IEnumerable<IFilter>> GetGlobalFilters = Enumerable.Empty<IFilter>;

        public static Func<ActionExecutionContext, IEnumerable<IFilter>> GetFiltersFor = context =>
        {
            var targetFilters = context.Target != null ? context.Target.GetType().GetAttributes<IFilter>(true) : Enumerable.Empty<IFilter>();
            var methodFilters = context.Method != null ? context.Method.GetAttributes<IFilter>(true) : Enumerable.Empty<IFilter>();
            return Enumerable.Empty<IFilter>()
                .Union(targetFilters)
                .Union(methodFilters)
                .Union(GetGlobalFilters())
                .OrderBy(x => x.Priority);
        };

        public static IResult WrapWith(this IResult inner, IEnumerable<IExecutionWrapper> wrappers)
        {
            return wrappers.Aggregate(inner, (current, wrapper) => wrapper.Wrap(current));
        }

        private static void ExecuteActionImpl(LambdaExpression exp, ActionExecutionContext context)
        {
            var call = exp.Body as MethodCallExpression;
            if (call != null)
            {
                context.Method = call.Method;
                var targetExp = call.Object as ConstantExpression;
                if (targetExp != null) context.Target = targetExp.Value;
            }
            FilterFramework.InvokeAction(context, new object[] { });
        }

        public static void ExecuteAction(Expression<System.Action> action)
        {
            var context = new ActionExecutionContext();
            context["expression"] = action;
            ExecuteActionImpl(action, context);
        }

        public static void ExecuteAction(Expression<Func<IEnumerable<IResult>>> coroutine)
        {
            var context = new ActionExecutionContext();
            context["expression"] = coroutine;
            ExecuteActionImpl(coroutine, context);
        }

        public static void ExecuteAction(Expression<Func<IEnumerator<IResult>>> coroutine)
        {
            var context = new ActionExecutionContext();
            context["expression"] = coroutine;
            ExecuteActionImpl(coroutine, context);
        }
    }

    public class ExecuteActionResult : IResult
    {
        private readonly object[] _parameters;

        public ExecuteActionResult(object[] parameters)
        {
            _parameters = parameters;
        }

        public object Outcome { get; private set; }

        public void Execute(ActionExecutionContext context)
        {
            LambdaExpression exp = null;
            try { exp = context["expression"] as LambdaExpression; } catch {}
            try
            {
                Outcome = exp == null ? context.Method.Invoke(context.Target, _parameters) : exp.Compile().DynamicInvoke();
                Completed(this, new ResultCompletionEventArgs());
            }
            catch(Exception ex)
            {
                Completed(this, new ResultCompletionEventArgs { Error = ex });
            }
        }

        public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };

        public IEnumerator<IResult> GetOutcomeEnumerator()
        {
            var single = Outcome as IResult;
            if(single != null)
                return Enumerable.Repeat(single, 1).GetEnumerator();

            var enumerable = Outcome as IEnumerable<IResult>;
            if(enumerable != null)
                return enumerable.GetEnumerator();

            return Outcome as IEnumerator<IResult>;
        }
    }

This also supports global filters, and also lifts the requirement to only call members:

FilterManager.ExecuteAction(() => _somePrivateMember.SomeCoroutine(someParameter, someOtherParameter));

Combined with for example a global SetBusy attribute, this provides great flexibility.

NB: I have not tested this heavily with respect to memory leakage ++

Rob: I wish ActionExecutionContext could return null instead of throwing an exception when a request is done for a key which is not present in the values dictionary... please... :-)

 

 

Coordinator
Mar 20, 2011 at 4:12 PM

I added a ticket for that.

Mar 20, 2011 at 9:03 PM

Hi janoveh, thanks for these improvements to the Filter framework; it definitely should support parameters, too.

I'll integrate it in the next few days (hopefully).