Benjamin Gale
Benjamin Gale

Reputation: 13177

How can I get my generic method to work?

I am writing a small extension to my WPF viewModel base class that allows me to define dependencies between properties in a cleaner way than the standard method of raising multiple PropertyChanged events in the getter:

So instead of:

public int ThisProperty
{  
    get
    {
        thisProperty = value;
        RaisePropertyChangedEvent("ThisProperty");
        RaisePropertyChangedEvent("FirstDependentProperty");
        RaisePropertyChangedEvent("SecondDependentProperty");
    }
}

I want to be able to do something like this in my ViewModels constructor:

RegisterDependencies("This Property", 
    "FirstDependentProperty", "SecondDependentProperty");

I have defined the following method in my viewModel (error checking removed to reduce the amount of code):

public void RegisterDependencies(string property, params string[] dependencies)
{
    foreach (string item in dependencies)
    {
        IList<string> deps;
        if (dependenciesList.TryGetValue(item, out deps))
        {
            if (!deps.Contains(property))
            {
                deps.Add(property);
            }
        }
        else
        {
            deps = new List<string>();
            deps.Add(property);
            dependenciesList[item] = deps;
        }
    }
}

My viewmodel subscribes to the PropertyChanged event with the following method:

void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    IList<string> dependencies;
    if (dependenciesList.TryGetValue(e.PropertyName, out dependencies))
    {
        foreach (string item in dependencies)
        {
            RaisePropertyChangedEvent(item);
        }
    }
}

This simply checks if the property has any dependencies, and if so, raises the PropertyChanged event for those as well. This all works beautifully, so far so good.

What I really want though is to use lambda expressions rather than strings so that I get auto-complete and better re-factoring support. What I'm after is something like this:

RegisterDependencies<ViewModelType>(p => p.Property, 
    p => p.FirstDependency,
    p => p.SecondDependency); //and so on...

I have redefined the method signature to look like this:

public void RegisterDependencies<T>(Expression<Func<T, object>> property, 
    prams Expression<Func<T, object>>[] dependencies)
    {
    }

At the moment the method simply attempts to convert the expressions to strings and this is where I'm coming unstuck. I'm not sure how to get the name of the property from the expression and convert it to a string.

Josh Smith's MVVM Foundation contains a class called PropertyChangedObserver which has a piece of code that does this which I have attempted to adapt to suit my example:

private static string GetPropertyName<T>(Expression<Func<T, object>> expression)
{
    var lambda = expression as LambdaExpression;

    MemberExpression memberExpression;
    if (lambda.Body is UnaryExpression)
    {
        var unaryExpression = lambda.Body as UnaryExpression;
        memberExpression = unaryExpression.Operand as MemberExpression;
    }
    else
    {
        memberExpression = lambda.Body as MemberExpression;
    }

    return memberExpression != null ? ((PropertyInfo)memberExpression.Member).Name : null;
}

This is being called in my updated RegisterDependencies method (this is literally the entire code for the method at the moment):

string propertyName = GetPropertyName(property);

foreach (Expression<Func<T, object>> expr in dependencies)
{
    string dependencyName = GetPropertyName(expr);
}

When this is run it results in a XamlParseException. This is difficult to debug as it doesn't throw up the standard exception window. Visual Studio's output window provides a bit more information and it seems the initial exception is a InvalidCastException although I'm not sure why as both expressions are of the same type.

Can anyone shed any light and help me with this?

Upvotes: 1

Views: 175

Answers (3)

Benjamin Gale
Benjamin Gale

Reputation: 13177

It turns out that somehow I'd managed to type the property names in wrong e.g. where I wanted:

p => p.Quantity

I'd actually typed:

p => p.quantity

notice the lower-case q. Due to the fact I was calling this in my constructor I also had access to all my private members (so my type had a field named quantity) which is why this was not causing the compiler to complain but an InvalidCastException to be generated at Runtime as it is obviously not a property.

@Phoog & Tilak - Thank you for your help. Both of your answers helped me isolate the problem which is why I've up-voted both of your answers despite this being the 'correct' one.

Upvotes: 0

Tilak
Tilak

Reputation: 30698

Seems Problem is in passing expression to RegisterDependencies.

Following (slightly adapted code) is successfully running.

    public static void Test()
    {
        var deps = RegisterDependencies((KeyValuePair<string,string> p) => p.Key, (KeyValuePair<string,string> p) => p.Value);
        foreach(var d in deps)
        {
            Console.WriteLine(d); // Prints Key, Then Value
        }
    }
    public static IEnumerable<string> RegisterDependencies<T>(Expression<Func<T, object>> property,    params Expression<Func<T, object>>[] dependencies)
    {
        var deps = new List<string>();
        deps.Add(GetPropertyName(property));
        foreach (var d in dependencies)
        {
            deps.Add(GetPropertyName(d));
        }
        return deps;
    }

    public static string GetPropertyName<T>(Expression<Func<T, object>> expression)
    {
        var lambda = expression as LambdaExpression;

        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambda.Body as MemberExpression;
        }

        return memberExpression != null ? ((PropertyInfo)memberExpression.Member).Name : null;
    }

Upvotes: 1

phoog
phoog

Reputation: 43046

Following up on Matti Virkkunen's comment, the Member property returns a MemberInfo object that can represent a property or a field. If that is your problem, no worries; the Name property is actually a member of the MemberInfo class, so there's no need to cast to PropertyInfo.

If something else is going on, we need more information to help; could you post the call site with the actual lambda expressions you're passing?

Upvotes: 1

Related Questions