LINQ表达式和扩展方法获取属性名称

4
我看了这篇介绍如何在POCO属性之间进行数据绑定的帖子:Data Binding POCO Properties。Bevan在评论中提供了一个简单的Binder类,用于实现此类数据绑定。它对我所需的功能非常有效,但我想实现Bevan提出的一些改进建议,包括:
  • 检查源和目标是否已分配
  • 检查源属性名和目标属性名是否存在
  • 检查两个属性之间的类型兼容性

此外,考虑到通过字符串指定属性容易出错,可以使用Linq表达式和扩展方法来替代。然后,您可以写成:

Binder.Bind( source, "Name", target, "Name")

你可以编写代码

source.Bind( Name => target.Name);

我相信我可以处理前三个问题(尽管您可以包括这些更改),但我不知道如何使用Linq表达式和扩展方法来编写代码,而不使用属性名字符串。

有什么技巧吗?

以下是在链接中找到的原始代码:

public static class Binder
{

    public static void Bind(
        INotifyPropertyChanged source,
        string sourcePropertyName,
        INotifyPropertyChanged target,
        string targetPropertyName)
    {
        var sourceProperty
            = source.GetType().GetProperty(sourcePropertyName);
        var targetProperty
            = target.GetType().GetProperty(targetPropertyName);

        source.PropertyChanged +=
            (s, a) =>
            {
                var sourceValue = sourceProperty.GetValue(source, null);
                var targetValue = targetProperty.GetValue(target, null);
                if (!Object.Equals(sourceValue, targetValue))
                {
                    targetProperty.SetValue(target, sourceValue, null);
                }
            };

        target.PropertyChanged +=
            (s, a) =>
            {
                var sourceValue = sourceProperty.GetValue(source, null);
                var targetValue = targetProperty.GetValue(target, null);
                if (!Object.Equals(sourceValue, targetValue))
                {
                    sourceProperty.SetValue(source, targetValue, null);
                }
            };
    }
}

5个回答

13

以下代码将从lambda表达式中返回属性名称的字符串:

public string PropertyName<TProperty>(Expression<Func<TProperty>> property)
{
  var lambda = (LambdaExpression)property;

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

  return memberExpression.Member.Name;
}

使用方法:

public class MyClass
{
  public int World { get; set; }
}

...
var c = new MyClass();
Console.WriteLine("Hello {0}", PropertyName(() => c.World));

更新

public static class Extensions
{
    public static void Bind<TSourceProperty, TDestinationProperty>(this INotifyPropertyChanged source, Expression<Func<TSourceProperty, TDestinationProperty>> bindExpression)
    {
        var expressionDetails = GetExpressionDetails<TSourceProperty, TDestinationProperty>(bindExpression);
        var sourcePropertyName = expressionDetails.Item1;
        var destinationObject = expressionDetails.Item2;
        var destinationPropertyName = expressionDetails.Item3;

        // Do binding here
        Console.WriteLine("{0} {1}", sourcePropertyName, destinationPropertyName);
    }

    private static Tuple<string, INotifyPropertyChanged, string> GetExpressionDetails<TSourceProperty, TDestinationProperty>(Expression<Func<TSourceProperty, TDestinationProperty>> bindExpression)
    {
        var lambda = (LambdaExpression)bindExpression;

        ParameterExpression sourceExpression = lambda.Parameters.FirstOrDefault();
        MemberExpression destinationExpression = (MemberExpression)lambda.Body;

        var memberExpression = destinationExpression.Expression as MemberExpression;
        var constantExpression = memberExpression.Expression as ConstantExpression;
        var fieldInfo = memberExpression.Member as FieldInfo;
        var destinationObject = fieldInfo.GetValue(constantExpression.Value) as INotifyPropertyChanged;

        return new Tuple<string, INotifyPropertyChanged, string>(sourceExpression.Name, destinationObject, destinationExpression.Member.Name);
    }
}

使用方法:

public class TestSource : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string Name { get; set; }        
}

public class TestDestination : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string Id { get; set; }    
}

class Program
{        
    static void Main(string[] args)
    {
        var x = new TestSource();
        var y = new TestDestination();

        x.Bind<string, string>(Name => y.Id);
    }    
}

谢谢您。您知道我需要做什么才能编写像原始问题中的代码一样的代码,例如source.Bind(Name => target.Name)?我不太清楚如何将上述Binder类和您的方法组合成扩展方法。 - Flack
我已经更新了答案,并提供了一个非常粗糙的实现。请注意,我还没有进行过太多(即任何)真正的测试! - devdigital
谢谢!希望您能帮我解决两个问题。当我调用Bind时,第二个参数是当前类的成员,所以我有类似x.Bind<string, string>(Name => Id)的东西,而不是x.Bind<string, string>(Name => y.Id)。在这种情况下,绑定失败,因为destinationExpression.Expression是一个ConstantExpression而不是MemberExpression。我不确定我需要改变什么才能使它在这种情况下工作。其次,是否有一种方法可以在编译时使其失败,如果属性名称不正确,例如x.Bind<string, string>(Na123me => y.Id)? - Flack

1
这个问题与从lambda表达式中检索属性名称非常相似。
(从https://dev59.com/-HRB5IYBdhLWcg3wSVYI#17220748转载的答案)
我不知道您是否需要绑定到“子属性”,但是检查lambda.Body中的Member.Name只会返回“最终”属性,而不是“完全限定”的属性。
例如:o => o.Thing1.Thing2将导致Thing2,而不是Thing1.Thing2
当尝试使用此方法简化EntityFramework DbSet.Include(string)的表达式重载时,这是有问题的。
因此,您可以“欺骗”并解析Expression.ToString。在我的测试中,性能似乎相当,如果这是一个坏主意,请纠正我。

扩展方法

/// <summary>
/// Given an expression, extract the listed property name; similar to reflection but with familiar LINQ+lambdas.  Technique @via https://dev59.com/AGYr5IYBdhLWcg3wi63d#16647343
/// </summary>
/// <remarks>Cheats and uses the tostring output -- Should consult performance differences</remarks>
/// <typeparam name="TModel">the model type to extract property names</typeparam>
/// <typeparam name="TValue">the value type of the expected property</typeparam>
/// <param name="propertySelector">expression that just selects a model property to be turned into a string</param>
/// <param name="delimiter">Expression toString delimiter to split from lambda param</param>
/// <param name="endTrim">Sometimes the Expression toString contains a method call, something like "Convert(x)", so we need to strip the closing part from the end</pa ram >
/// <returns>indicated property name</returns>
public static string GetPropertyName<TModel, TValue>(this Expression<Func<TModel, TValue>> propertySelector, char delimiter = '.', char endTrim = ')') {

    var asString = propertySelector.ToString(); // gives you: "o => o.Whatever"
    var firstDelim = asString.IndexOf(delimiter); // make sure there is a beginning property indicator; the "." in "o.Whatever" -- this may not be necessary?

    return firstDelim < 0
        ? asString
        : asString.Substring(firstDelim+1).TrimEnd(endTrim);
}//--   fn  GetPropertyNameExtended

(检查分隔符甚至可能有点过度)


0

这可能不完全是您要求的,但我已经做了类似的事情来处理两个对象之间属性的映射:

public interface IModelViewPropagationItem<M, V>
    where M : BaseModel
    where V : IView
{
    void SyncToView(M model, V view);
    void SyncToModel(M model, V view);
}

public class ModelViewPropagationItem<M, V, T> : IModelViewPropagationItem<M, V>
    where M : BaseModel
    where V : IView
{
    private delegate void VoidDelegate();

    public Func<M, T> ModelValueGetter { get; private set; }
    public Action<M, T> ModelValueSetter { get; private set; }
    public Func<V, T> ViewValueGetter { get; private set; }
    public Action<V, T> ViewValueSetter { get; private set; }

    public ModelViewPropagationItem(Func<M, T> modelValueGetter, Action<V, T> viewValueSetter)
        : this(modelValueGetter, null, null, viewValueSetter)
    { }

    public ModelViewPropagationItem(Action<M, T> modelValueSetter, Func<V, T> viewValueGetter)
        : this(null, modelValueSetter, viewValueGetter, null)
    { }

    public ModelViewPropagationItem(Func<M, T> modelValueGetter, Action<M, T> modelValueSetter, Func<V, T> viewValueGetter, Action<V, T> viewValueSetter)
    {
        this.ModelValueGetter = modelValueGetter;
        this.ModelValueSetter = modelValueSetter;
        this.ViewValueGetter = viewValueGetter;
        this.ViewValueSetter = viewValueSetter;
    }

    public void SyncToView(M model, V view)
    {
        if (this.ViewValueSetter == null || this.ModelValueGetter == null)
            throw new InvalidOperationException("Syncing to View is not supported for this instance.");

        this.ViewValueSetter(view, this.ModelValueGetter(model));
    }

    public void SyncToModel(M model, V view)
    {
        if (this.ModelValueSetter == null || this.ViewValueGetter == null)
            throw new InvalidOperationException("Syncing to Model is not supported for this instance.");

        this.ModelValueSetter(model, this.ViewValueGetter(view));
    }
}

这允许您创建此对象的实例,然后使用“SyncToModel”和“SyncToView”来来回移动值。以下与此相关的代码片段允许您将多个这些对象分组,并通过一个调用来回移动数据:

public class ModelViewPropagationGroup<M, V> : List<IModelViewPropagationItem<M, V>>
    where M : BaseModel
    where V : IView
{
    public ModelViewPropagationGroup(params IModelViewPropagationItem<M, V>[] items)
    {
        this.AddRange(items);
    }

    public void SyncAllToView(M model, V view)
    {
        this.ForEach(o => o.SyncToView(model, view));
    }

    public void SyncAllToModel(M model, V view)
    {
        this.ForEach(o => o.SyncToModel(model, view));
    }
}

使用方法大致如下:

private static readonly ModelViewPropagationItem<LoginModel, ILoginView, string> UsernamePI = new ModelViewPropagationItem<LoginModel, ILoginView, string>(m => m.Username.Value, (m, x) => m.Username.Value = x, v => v.Username, (v, x) => v.Username = x);
private static readonly ModelViewPropagationItem<LoginModel, ILoginView, string> PasswordPI = new ModelViewPropagationItem<LoginModel, ILoginView, string>(m => m.Password.Value, (m, x) => m.Password.Value = x, v => v.Password, (v, x) => v.Password = x);
private static readonly ModelViewPropagationGroup<LoginModel, ILoginView> GeneralPG = new ModelViewPropagationGroup<LoginModel, ILoginView>(UsernamePI, PasswordPI);

public UserPrincipal Login_Click()
{
    GeneralPG.SyncAllToModel(this.Model, this.View);

    return this.Model.DoLogin();
}

希望这能帮到你!


0

var pr = typeof(CCategory).GetProperties().Select(i => i.Name).ToList();

变量pr等于typeof(CCategory).GetProperties().Select(i => i.Name).ToList();


很不错的尝试,但编译器并不知道在运行时将生成的列表内容。因此,在重构等方面没有智能感知优势。 - Mike de Klerk

0

声明:

    class Foo<T> {
            public string Bar<T, TResult>(Expression<Func<T, TResult>> expersion)
            {
                var lambda = (LambdaExpression)expersion;
                MemberExpression memberExpression;
                if (lambda.Body is UnaryExpression)
                {
                    var unaryExpression = (UnaryExpression)lambda.Body;
                    memberExpression = (MemberExpression)unaryExpression.Operand;
                }
                else
                {
                    memberExpression = (MemberExpression)lambda.Body;
                }

                return memberExpression.Member.Name;
            }
    }

使用方法:

    var foo = new Foo<DummyType>();
    var propName = foo.Bar(d=>d.DummyProperty)
    Console.WriteLine(propName); //write "DummyProperty" string in shell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接