Bugs in Action filters?

Sep 22, 2010 at 1:07 PM

Hello,

I'm converting a project I made with Caliburn, to now use CM.

I must use the action filters, and copy/pasted the code you give here : http://caliburnmicro.codeplex.com/wikipage?title=Action%20Filters&referringTitle=Documentation

Anyway, I have an ArgumentNullException which is thrown when I load my first view model. Here is the stacktrace:

at System.Attribute.GetCustomAttributes(MemberInfo element, Boolean inherit)  

at Caliburn.Micro.ExtensionMethods.GetAttributes[T](MemberInfo member, Boolean inherit) in D:\__PROG STUFF__\VS\2010\Projects\emidee\lib\caliburn.micro\src\Caliburn.Micro.Silverlight\ExtensionMethods.cs:line 27  

at Emidee.Caliburn.Filters.FilterManager.<.cctor>b__2(ActionExecutionContext context) in D:\__PROG STUFF__\VS\2010\Projects\emidee\src\app\Emidee.Caliburn\Filters\FilterManager.cs:line 10  

at Emidee.Caliburn.Filters.FilterFramework.PrepareContext(ActionExecutionContext context) in D:\__PROG STUFF__\VS\2010\Projects\emidee\src\app\Emidee.Caliburn\Filters\FilterFramework.cs:line 24  

at Emidee.Caliburn.Filters.FilterFramework.<>c__DisplayClass1.b__0(ActionExecutionContext context) in D:\__PROG STUFF__\VS\2010\Projects\emidee\src\app\Emidee.Caliburn\Filters\FilterFramework.cs:line 16  

at Caliburn.Micro.ActionMessage.UpdateContext() in D:\__PROG STUFF__\VS\2010\Projects\emidee\lib\caliburn.micro\src\Caliburn.Micro.Silverlight\ActionMessage.cs:line 137  

at Caliburn.Micro.ActionMessage.ElementLoaded(Object sender, RoutedEventArgs e) in D:\__PROG STUFF__\VS\2010\Projects\emidee\lib\caliburn.micro\src\Caliburn.Micro.Silverlight\ActionMessage.cs:line 126  

at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)   at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)  

at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)   at System.Windows.UIElement.RaiseEvent(RoutedEventArgs e)  

The problem lies in the GetFiltersFor property of FilterManager, in the Union extension method, where context.Method is null.

I've corrected this by this code:

public static Func<ActionExecutionContext, IEnumerable<IFilter>> GetFiltersFor = context =>
        {
        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;
        };

Which doesn't throw anymore, but maybe this is not the most optimal method.

The second bug I have is related to the Dependencies attribute.

I have this view-model:

public abstract class EditEntityViewModel<TEntity, TEntitiesRepository> : EmideeScreenWithSession, IViewFocus
    where TEntity : BaseEntity<TEntity>, new()
    where TEntitiesRepository : IRepository<TEntity>
{
    public TEntity SelectedEntity { get; private set; }

    public bool CanEditOrSave { get { return SelectedEntity != null && SelectedEntity.Error == string.Empty; } }

    [Dependencies("SelectedEntity.*")]
    public IEnumerable<IResult> EditOrSave()
    {
        // Do some stuff...
    }
}

Which used to work with caliburn. When I edit one of the properties of SelectedEntity, it was calling the CanEditOrSave property, which isn't done anymore with CM.

Is this a bug?

Thanks in advance

Mike

Coordinator
Sep 22, 2010 at 1:16 PM
Edited Sep 22, 2010 at 1:23 PM

A couple of things:

1. Technically, we don't officially support recipes. So, if you have bugs...you may or may not get help on that. It's not our top priority.

2. However. The recipe was created against a slightly earlier version of the framework and some things have probably changed since then causing your error. I will try to get that fixed, but I cannot make any promises. In the mean time, your change will probably work.

3. The Dependencies implementation in that recipe does not support nested or wildcard paths. You will have to extend that implementation if you want anything other than basic property tracking.

Sep 22, 2010 at 1:42 PM

Thanks for your quick answer.

Here is what I've just written. That works, but I suspect this is far from being optimal. You're warned about it :p

public class DependenciesAttribute : Attribute, IContextAware
{
    private ActionExecutionContext context;
    private INotifyPropertyChanged target;
    private IList<INotifyPropertyChanged> propertyTargets = new List<INotifyPropertyChanged>();

    public DependenciesAttribute(params string[] propertyNames)
    {
        PropertyNames = propertyNames ?? new string[] { };
    }

    public string[] PropertyNames { get; private set; }
    public int Priority { get; set; }

    public void MakeAwareOf(ActionExecutionContext context)
    {
        this.context = context;
        target = context.Target as INotifyPropertyChanged;

        if (target != null)
            target.PropertyChanged += TargetPropertyChanged;

        foreach (var propertyName in PropertyNames)
            if (propertyName.EndsWith(".*"))
            {
                var name = propertyName.Substring(0, propertyName.Length - 2);

                var propertyValue = context.Target.GetType().GetProperty(name).GetValue(target, null);

                var propertyTarget = propertyValue as INotifyPropertyChanged;

                if (propertyTarget != null)
                {
                    propertyTargets.Add(propertyTarget);
                    propertyTarget.PropertyChanged += PropertyTargetPropertyChanged;
                }
            }
    }

    public void Dispose()
    {
        if (target != null)
            target.PropertyChanged -= TargetPropertyChanged;

        propertyTargets.ForEach(inpc => inpc.PropertyChanged -= PropertyTargetPropertyChanged);

        target = null;
    }

    void TargetPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (string.IsNullOrEmpty(e.PropertyName) || PropertyNames.Contains(e.PropertyName))
            UpdateAvailability();
    }

    void PropertyTargetPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        UpdateAvailability();
    }

    void UpdateAvailability()
    {
        Execute.OnUIThread(() => context.Message.UpdateAvailability());
    }
}

 

Any thoughts?

Sep 23, 2010 at 2:52 PM

Well, I have to temper my previous post, as it doesn't work (surprisingly :p) in all cases.

For example, if the dependent property is a proxy returned by NHibernate, that doesn't work, where as it used to with the full Caliburn.

I'll have to investigate this deeper, and have a look on how it is done in Caliburn.

Sep 27, 2010 at 1:38 PM

Here is the last implementation I came up with, which allows to be dependent on other properties, or properties of a property:

public class DependenciesAttribute : Attribute, IContextAware
{
    private ActionExecutionContext context;
    private INotifyPropertyChanged target;

    private readonly string[] dependentProperties;
    private readonly IList<string> propertyNames = new List<string>();
    private readonly IList<Tuple<INotifyPropertyChanged, string>> propertyTargets = new List<Tuple<INotifyPropertyChanged, string>>();

    public DependenciesAttribute(params string[] propertyNames)
    {
        dependentProperties = propertyNames ?? new string[] { };
    }

    public int Priority { get; set; }

    public void MakeAwareOf(ActionExecutionContext context)
    {
        this.context = context;
        target = context.Target as INotifyPropertyChanged;

        if (target != null)
            target.PropertyChanged += TargetPropertyChanged;

        foreach (var parts in dependentProperties.Select(propertyName => propertyName.Split('.')))
        {
            if (parts.Length == 1)
            {
                propertyNames.Add(parts[0]);
                continue;
            }

            var name = parts[0];
            var propertyValue = context.Target.GetType().GetProperty(name).GetValue(target, null);

            var propertyTarget = propertyValue as INotifyPropertyChanged;

            if (propertyTarget == null) 
                continue;

            var propsOfProperty = parts[1];

            propertyTargets.Add(new Tuple<INotifyPropertyChanged, string>(propertyTarget, propsOfProperty));
            propertyTarget.PropertyChanged += PropertyTargetPropertyChanged;
        }
    }

    public void Dispose()
    {
        if (target != null)
            target.PropertyChanged -= TargetPropertyChanged;

        propertyTargets.ForEach(inpc => inpc.Item1.PropertyChanged -= PropertyTargetPropertyChanged);

        target = null;
    }

    void TargetPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (string.IsNullOrEmpty(e.PropertyName) || propertyNames.Contains(e.PropertyName))
            UpdateAvailability();
    }

    void PropertyTargetPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var inpc = (from propertyTarget in propertyTargets
                    where propertyTarget.Item1 == sender
                    select propertyTarget).FirstOrDefault();

        if (inpc == null)
            return;

        if (inpc.Item2 == "*" || string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == inpc.Item2)
            UpdateAvailability();
    }

    void UpdateAvailability()
    {
        Execute.OnUIThread(() => context.Message.UpdateAvailability());
    }
}

I have a problem with the following behavior:

I have a conductor view-model which acts like a wizard (multiple pages which are accessed with Next / Previous, and so on...)

Each of the view-models which are displayed inside the conductor all implement the IDataErrorInfo interface.

The conductor has the following methods and guards:

public bool CanGoToPreviousPage { get { return !(ActiveItem is FirstWizardViewModel); } }

public bool CanGoToNextPage
{
    get
    {
        IDataErrorInfo errorInfo = ActiveItem as IDataErrorInfo;

        return errorInfo == null || string.IsNullOrEmpty(errorInfo.Error);
    }
}

[Dependencies("ActiveItem", "ActiveItem.*")]
public IEnumerable<IResult> GoToNextPage()
{
    IScreen nextScreen = null;
            
    if (ActiveItem is FirstWizardViewModel)
        nextScreen = IoC.Get<SecondWizardViewModel>();
           
    yield return Show.Child(nextScreen).In(this);
}

[Dependencies("ActiveItem", "ActiveItem.*")]
public IEnumerable<IResult> GoToPreviousPage()
{
    IScreen previousScreen = null;

    if (ActiveItem is SecondWizardViewModel)
        previousScreen = IoC.Get<FirstWizardViewModel>();

    yield return Show.Child(previousScreen).In(this);
}

When I open the wizard, I activate the first view-model, and the validation is correctly done (the Next button is disabled).

When I fill the fields, and click on Next, the second view-model is opened.

If I click on Previous to activate the first VM, and cause the validation to fail, the Next button remains enabled, whereas the IDataErrorInfo.Error property is correct and the NotifyPropertyChanged event is fired.

I've put a breakpoint in the Dependencies code, and I don't go through the PropertyTargetPropertyChanged method.

Doesn't the MakeAwareOf method of the filter be called when I re-open the view-model? Its only done when I open the VM for the first time.

I hope I have been clear enough :)

Thanks in advance

Mike

Oct 4, 2010 at 4:13 PM

I've written a blog post on how I solved my problems : http://www.emidee.net/blog/index.php/post/2010/10/04/[Caliburn-Micro]-Action-filters-Dependencies-on-properties-of-properties

If it can help any one here ;)