Pobieranie nazwy właściwości z wyrażenia lambda

512

Czy istnieje lepszy sposób na uzyskanie nazwy właściwości po przekazaniu za pomocą wyrażenia lambda? Oto, co aktualnie mam.

na przykład.

GetSortingInfo<User>(u => u.UserId);

Działa poprzez rzutowanie go jako wyrażenia membranowego tylko wtedy, gdy właściwość jest ciągiem znaków. ponieważ nie wszystkie właściwości są łańcuchami, musiałem użyć obiektu, ale wtedy zwróciłby dla nich jednoargumentowe wyrażenie.

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> action) where T : class
{
    var expression = GetMemberInfo(action);
    string name = expression.Member.Name;

    return GetInfo(html, name);
}

private static MemberExpression GetMemberInfo(Expression method)
{
    LambdaExpression lambda = method as LambdaExpression;
    if (lambda == null)
        throw new ArgumentNullException("method");

    MemberExpression memberExpr = null;

    if (lambda.Body.NodeType == ExpressionType.Convert)
    {
        memberExpr = 
            ((UnaryExpression)lambda.Body).Operand as MemberExpression;
    }
    else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
    {
        memberExpr = lambda.Body as MemberExpression;
    }

    if (memberExpr == null)
        throw new ArgumentException("method");

    return memberExpr;
}
Schotime
źródło
Lepszy jak w ładniejszym kodzie? Nie wydaje mi się Sprawdzanie typu rozciąga się tylko na ogólne wyrażenie, więc naprawdę potrzebujesz sprawdzeń, które masz w czasie wykonywania. :(
MichaelGG
Tak ... zastanawiałem się tylko, czy istnieje lepszy sposób na zrobienie tego, ponieważ wydawało mi się to trochę zrzędliwe. Ale jeśli tak, to fajnie. dzięki.
Schotime
Zaktualizowałem twój komentarz; ale użycie lambda, aby uzyskać ciąg, dzięki czemu można użyć dynamicznego LINQ, uderza mnie jako działanie wstecz ... jeśli używasz lambda, użyj lambda ;-p Nie musisz wykonywać całego zapytania w jednym kroku - możesz użyć „zwykłego / lambda” OrderBy, „dynamicznego LINQ / łańcucha” Gdzie itd.
Marc Gravell
4
Uwaga dla wszystkich: Zastosuj MemberExpressionwymienione tutaj podejście tylko do uzyskania nazwy członka, a nie do uzyskania MemberInfosamego samego, ponieważ MemberInfonie ma gwarancji , że zwracany typ będzie odzwierciedlony w niektórych scenariuszach „dervied: base”. Zobacz lambda-expression-not-return-expect-memberinfo . Potknąłem się raz. Cierpi na to również zaakceptowana odpowiedź.
nawfal

Odpowiedzi:

350

Niedawno zrobiłem bardzo podobną rzecz, aby uczynić typ bezpieczną metodą OnPropertyChanged.

Oto metoda, która zwróci obiekt PropertyInfo dla wyrażenia. Zgłasza wyjątek, jeśli wyrażenie nie jest właściwością.

public PropertyInfo GetPropertyInfo<TSource, TProperty>(
    TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda)
{
    Type type = typeof(TSource);

    MemberExpression member = propertyLambda.Body as MemberExpression;
    if (member == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));

    if (type != propInfo.ReflectedType &&
        !type.IsSubclassOf(propInfo.ReflectedType))
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));

    return propInfo;
}

Ten sourceparametr jest używany, aby kompilator mógł wnioskować o typie w wywołaniu metody. Możesz wykonać następujące czynności

var propertyInfo = GetPropertyInfo(someUserObject, u => u.UserID);
Cameron MacFarland
źródło
6
Dlaczego jest tam ostatnia kontrola dotycząca TSource? Lambda jest silnie napisana, więc nie sądzę, że jest to konieczne.
HappyNomad
16
Ponadto od 2012 r. Wnioskowanie typu działa poprawnie bez parametru source.
HappyNomad
4
@HappyNomad Wyobraź sobie obiekt, który ma jako element członkowski, instancję trzeciego typu. u => u.OtherType.OtherTypesPropertystworzy taki przypadek, który sprawdza ostatnia instrukcja.
joshperry 12.12.12
5
Ostatnią instrukcją if powinno być: if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType) && !propInfo.ReflectedType.IsAssignableFrom(type))aby umożliwić także interfejsy.
Graham King,
8
@GrayKing nie byłoby tym samym, co tylko if(!propInfo.ReflectedType.IsAssignableFrom(type))?
Connell
192

Znalazłem inny sposób, w jaki można to zrobić, polegający na silnym wpisaniu źródła i właściwości oraz jawnym wywnioskowaniu danych wejściowych dla lambda. Nie jestem pewien, czy jest to poprawna terminologia, ale oto wynik.

public static RouteValueDictionary GetInfo<T,P>(this HtmlHelper html, Expression<Func<T, P>> action) where T : class
{
    var expression = (MemberExpression)action.Body;
    string name = expression.Member.Name;

    return GetInfo(html, name);
}

A potem tak to nazwać.

GetInfo((User u) => u.UserId);

i voila to działa.
Dziękuje wszystkim.

Schotime
źródło
4
To rozwiązanie powinno być nieco zaktualizowane. Sprawdź następujący artykuł - oto link
Pavel Cermak
1
Jest to jedyna opcja, jeśli wykonujesz ASP.Net MVC i tylko dla warstwy interfejsu użytkownika (HtmlHelper).
Marc
3
począwszy od c # 6.0, którego możesz używaćGetInfo(nameof(u.UserId))
Vladislav
1
W rdzeniu netto musiałem użyć tego:var name = ((MemberExpression) ((UnaryExpression) accessor.Body).Operand).Member.Name
Falk
146

Bawiłem się tym samym i pracowałem nad tym. Nie jest w pełni przetestowany, ale wydaje się, że radzi sobie z problemem z typami wartości (problem z jednorazowym wyrażeniem, na jaki się natknąłeś)

public static string GetName(Expression<Func<object>> exp)
{
    MemberExpression body = exp.Body as MemberExpression;

    if (body == null) {
       UnaryExpression ubody = (UnaryExpression)exp.Body;
       body = ubody.Operand as MemberExpression;
    }

    return body.Member.Name;
}
M. Thelen
źródło
2
wypróbowałem to niedawno (z innego pytania ), okazało się, że nie obsługuje on właściwości podrzędnych: o => o.Thing1.Thing2zwróci Thing2, nie Thing1.Thing2, co jest niepoprawne, jeśli próbujesz użyć go w EntityFramework obejmuje
drzaus
1
AKA (field.Body is UnaryExpression? (((UnaryExpression) field.Body) .Operand: field.Body) as MemberExpression
51
public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    return (Field.Body as MemberExpression ?? ((UnaryExpression)Field.Body).Operand as MemberExpression).Member.Name;
}

Obsługuje wyrażenia elementarne i jednoargumentowe. Różnica polega na tym, że otrzymasz UnaryExpressionwyrażenie, jeśli wyrażenie reprezentuje typ wartości, natomiast otrzymasz MemberExpressionwyrażenie, jeśli wyrażenie reprezentuje typ odwołania. Wszystko można rzutować na obiekt, ale typy wartości muszą być zapakowane w ramki. Właśnie dlatego istnieje UnaryExpression. Odniesienie.

Ze względu na czytelność (@Jowen), oto rozwinięty odpowiednik:

public string GetName<TSource, TField>(Expression<Func<TSource, TField>> Field)
{
    if (object.Equals(Field, null))
    {
        throw new NullReferenceException("Field is required");
    }

    MemberExpression expr = null;

    if (Field.Body is MemberExpression)
    {
        expr = (MemberExpression)Field.Body;
    }
    else if (Field.Body is UnaryExpression)
    {
        expr = (MemberExpression)((UnaryExpression)Field.Body).Operand;
    }
    else
    {
        const string Format = "Expression '{0}' not supported.";
        string message = string.Format(Format, Field);

        throw new ArgumentException(message, "Field");
    }

    return expr.Member.Name;
}
Paul Fleming
źródło
@ flem, pomijam <TField> dla czytelności, czy jest jakiś problem. LambdaExpressions.GetName <Basket> (m => m.Quantity)
Soren
1
@soren Jestem pewien, że ktoś bardziej dostrojony ode mnie może zasugerować, że otwierasz swój kod na potencjał niepotrzebnego boksowania / rozpakowywania podczas przekazywania wyrażeń typów wartości, ale ponieważ wyrażenie nigdy nie jest kompilowane i oceniane w tej metodzie, to chyba nie jest problem.
Paul Fleming
29

Z dopasowaniem wzoru C # 7:

public static string GetMemberName<T>(this Expression<T> expression)
{
    switch (expression.Body)
    {
        case MemberExpression m:
            return m.Member.Name;
        case UnaryExpression u when u.Operand is MemberExpression m:
            return m.Member.Name;
        default:
            throw new NotImplementedException(expression.GetType().ToString());
    }
}

Przykład:

public static RouteValueDictionary GetInfo<T>(this HtmlHelper html, 
    Expression<Func<T, object>> action) where T : class
{
    var name = action.GetMemberName();
    return GetInfo(html, name);
}

[Aktualizacja] Dopasowanie wzorca C # 8:

public static string GetMemberName<T>(this Expression<T> expression) =>
    expression.Body switch
    {
        MemberExpression m =>
            m.Member.Name,
        UnaryExpression u when u.Operand is MemberExpression m =>
            m.Member.Name,
        _ =>
            throw new    NotImplementedException(expression.GetType().ToString())
    };
akhansari
źródło
20

Jest to ogólna implementacja służąca do uzyskania nazwy ciągu pól / właściwości / indeksatorów / metod / metod rozszerzenia / delegatów struct / class / interface / delegate / array. Testowałem z kombinacjami wariantów statycznych / instancji i nietypowych / ogólnych.

//involves recursion
public static string GetMemberName(this LambdaExpression memberSelector)
{
    Func<Expression, string> nameSelector = null;  //recursive func
    nameSelector = e => //or move the entire thing to a separate recursive method
    {
        switch (e.NodeType)
        {
            case ExpressionType.Parameter:
                return ((ParameterExpression)e).Name;
            case ExpressionType.MemberAccess:
                return ((MemberExpression)e).Member.Name;
            case ExpressionType.Call:
                return ((MethodCallExpression)e).Method.Name;
            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
                return nameSelector(((UnaryExpression)e).Operand);
            case ExpressionType.Invoke:
                return nameSelector(((InvocationExpression)e).Expression);
            case ExpressionType.ArrayLength:
                return "Length";
            default:
                throw new Exception("not a proper member selector");
        }
    };

    return nameSelector(memberSelector.Body);
}

To też można napisać w prostej whilepętli:

//iteration based
public static string GetMemberName(this LambdaExpression memberSelector)
{
    var currentExpression = memberSelector.Body;

    while (true)
    {
        switch (currentExpression.NodeType)
        {
            case ExpressionType.Parameter:
                return ((ParameterExpression)currentExpression).Name;
            case ExpressionType.MemberAccess:
                return ((MemberExpression)currentExpression).Member.Name;
            case ExpressionType.Call:
                return ((MethodCallExpression)currentExpression).Method.Name;
            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
                currentExpression = ((UnaryExpression)currentExpression).Operand;
                break;
            case ExpressionType.Invoke:
                currentExpression = ((InvocationExpression)currentExpression).Expression;
                break;
            case ExpressionType.ArrayLength:
                return "Length";
            default:
                throw new Exception("not a proper member selector");
        }
    }
}

Lubię podejście rekurencyjne, chociaż drugie może być łatwiejsze do odczytania. Można to nazwać tak:

someExpr = x => x.Property.ExtensionMethod()[0]; //or
someExpr = x => Static.Method().Field; //or
someExpr = x => VoidMethod(); //or
someExpr = () => localVariable; //or
someExpr = x => x; //or
someExpr = x => (Type)x; //or
someExpr = () => Array[0].Delegate(null); //etc

string name = someExpr.GetMemberName();

aby wydrukować ostatniego członka.

Uwaga:

  1. W przypadku takich wyrażeń łańcuchowych, jak A.B.C„C” jest zwracane.

  2. Nie działa to z consts, indeksatorami tablic lub enums (niemożliwe jest uwzględnienie wszystkich przypadków).

nawfal
źródło
19

Istnieje przypadek na krawędzi, jeśli chodzi o Array.Length. Chociaż „Długość” jest widoczna jako właściwość, nie można jej używać w żadnym z wcześniej proponowanych rozwiązań.

using Contract = System.Diagnostics.Contracts.Contract;
using Exprs = System.Linq.Expressions;

static string PropertyNameFromMemberExpr(Exprs.MemberExpression expr)
{
    return expr.Member.Name;
}

static string PropertyNameFromUnaryExpr(Exprs.UnaryExpression expr)
{
    if (expr.NodeType == Exprs.ExpressionType.ArrayLength)
        return "Length";

    var mem_expr = expr.Operand as Exprs.MemberExpression;

    return PropertyNameFromMemberExpr(mem_expr);
}

static string PropertyNameFromLambdaExpr(Exprs.LambdaExpression expr)
{
         if (expr.Body is Exprs.MemberExpression)   return PropertyNameFromMemberExpr(expr.Body as Exprs.MemberExpression);
    else if (expr.Body is Exprs.UnaryExpression)    return PropertyNameFromUnaryExpr(expr.Body as Exprs.UnaryExpression);

    throw new NotSupportedException();
}

public static string PropertyNameFromExpr<TProp>(Exprs.Expression<Func<TProp>> expr)
{
    Contract.Requires<ArgumentNullException>(expr != null);
    Contract.Requires<ArgumentException>(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression);

    return PropertyNameFromLambdaExpr(expr);
}

public static string PropertyNameFromExpr<T, TProp>(Exprs.Expression<Func<T, TProp>> expr)
{
    Contract.Requires<ArgumentNullException>(expr != null);
    Contract.Requires<ArgumentException>(expr.Body is Exprs.MemberExpression || expr.Body is Exprs.UnaryExpression);

    return PropertyNameFromLambdaExpr(expr);
}

Teraz przykładowe użycie:

int[] someArray = new int[1];
Console.WriteLine(PropertyNameFromExpr( () => someArray.Length ));

Gdyby PropertyNameFromUnaryExprnie sprawdził ArrayLength, „someArray” zostałby wydrukowany na konsoli (kompilator wydaje się generować bezpośredni dostęp do pola Długość kopii zapasowej , jako optymalizacja, nawet w Debugowaniu, a więc w specjalnym przypadku).

kornman00
źródło
16

Oto aktualizacja metody zaproponowanej przez Camerona . Pierwszy parametr nie jest wymagany.

public PropertyInfo GetPropertyInfo<TSource, TProperty>(
    Expression<Func<TSource, TProperty>> propertyLambda)
{
    Type type = typeof(TSource);

    MemberExpression member = propertyLambda.Body as MemberExpression;
    if (member == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));

    if (type != propInfo.ReflectedType &&
        !type.IsSubclassOf(propInfo.ReflectedType))
        throw new ArgumentException(string.Format(
            "Expresion '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));

    return propInfo;
}

Możesz wykonać następujące czynności:

var propertyInfo = GetPropertyInfo<SomeType>(u => u.UserID);
var propertyInfo = GetPropertyInfo((SomeType u) => u.UserID);

Metody rozszerzenia:

public static PropertyInfo GetPropertyInfo<TSource, TProperty>(this TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda) where TSource : class
{
    return GetPropertyInfo(propertyLambda);
}

public static string NameOfProperty<TSource, TProperty>(this TSource source,
    Expression<Func<TSource, TProperty>> propertyLambda) where TSource : class
{
    PropertyInfo prodInfo = GetPropertyInfo(propertyLambda);
    return prodInfo.Name;
}

Możesz:

SomeType someInstance = null;
string propName = someInstance.NameOfProperty(i => i.Length);
PropertyInfo propInfo = someInstance.GetPropertyInfo(i => i.Length);
Adrian
źródło
Nie, nie będzie on wnioskować ujako jakiś typ, nie może tego zrobić, ponieważ nie ma typu do wnioskowania. Co możesz zrobić, toGetPropertyInfo<SomeType>(u => u.UserID)
Lucas
14

Odkryłem, że niektóre z sugerowanych odpowiedzi, które przechodzą do MemberExpression/ UnaryExpressionnie przechwytują zagnieżdżonych / pod właściwości.

ex) o => o.Thing1.Thing2zwraca Thing1raczej niż Thing1.Thing2.

To rozróżnienie jest ważne, jeśli próbujesz pracować z EntityFramework DbSet.Include(...).

Przekonałem się, że samo parsowanie Expression.ToString()wydaje się działać dobrze i stosunkowo szybko. Porównałem go z UnaryExpressionwersją, a nawet zszedłem ToStringz tego, Member/UnaryExpressionaby sprawdzić, czy to było szybsze, ale różnica była znikoma. Popraw mnie, jeśli to okropny pomysł.

Metoda rozszerzenia

/// <summary>
/// Given an expression, extract the listed property name; similar to reflection but with familiar LINQ+lambdas.  Technique @via https://stackoverflow.com/a/16647343/1037948
/// </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</param>
/// <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

(Sprawdzanie separatora może być nawet nadmierne)

Demo (LinqPad)

Demonstracja + kod porównawczy - https://gist.github.com/zaus/6992590

drzaus
źródło
1
+ 1 bardzo interesujące. Czy nadal używasz tej metody we własnym kodzie? czy to działa dobrze? odkryłeś jakieś przypadki krawędzi?
Benjamin Gale,
Nie widzę twojego pomysłu. Odpowiedź, którą podałeś o => o.Thing1.Thing2, nie powraca, Thing1jak mówisz, ale Thing2. W rzeczywistości twoja odpowiedź zwraca coś, Thing1.Thing2co może być pożądane lub nie.
nawfal
Nie działa z przypadkami ostrzeżeń Kormana: stackoverflow.com/a/11006147/661933 . Zawsze lepiej unikać włamań.
nawfal
@nawfal # 1 - oryginalnym problemem jest to, że chcesz Thing1.Thing2 , nigdy Thing1. Powiedziałem, Thing2co oznacza, że wartość z o.Thing1.Thing2, co jest punktem orzecznika. Zaktualizuję odpowiedź, aby odzwierciedlić ten zamiar.
drzaus
@drzaus przepraszam, wciąż cię nie dostaję. Naprawdę próbuję zrozumieć. Dlaczego powiedziałbyś, że inne odpowiedzi tutaj wracają Thing1? Nie sądzę, żeby to w ogóle przestało działać.
nawfal
6

Używam metody rozszerzenia dla projektów sprzed C # 6 i nameof () dla tych, którzy celują w C # 6.

public static class MiscExtentions
{
    public static string NameOf<TModel, TProperty>(this object @object, Expression<Func<TModel, TProperty>> propertyExpression)
    {
        var expression = propertyExpression.Body as MemberExpression;
        if (expression == null)
        {
            throw new ArgumentException("Expression is not a property.");
        }

        return expression.Member.Name;
    }
}

I nazywam to tak:

public class MyClass 
{
    public int Property1 { get; set; }
    public string Property2 { get; set; }
    public int[] Property3 { get; set; }
    public Subclass Property4 { get; set; }
    public Subclass[] Property5 { get; set; }
}

public class Subclass
{
    public int PropertyA { get; set; }
    public string PropertyB { get; set; }
}

// result is Property1
this.NameOf((MyClass o) => o.Property1);
// result is Property2
this.NameOf((MyClass o) => o.Property2);
// result is Property3
this.NameOf((MyClass o) => o.Property3);
// result is Property4
this.NameOf((MyClass o) => o.Property4);
// result is PropertyB
this.NameOf((MyClass o) => o.Property4.PropertyB);
// result is Property5
this.NameOf((MyClass o) => o.Property5);

Działa dobrze zarówno z polami, jak i właściwościami.

kalitsov
źródło
5

Cóż, nie trzeba dzwonić .Name.ToString(), ale ogólnie o to chodzi, tak. Jedyne, czego możesz potrzebować, to czy x.Foo.Barpowinieneś zwrócić „Foo”, „Bar” lub wyjątek - tj. Czy w ogóle musisz iterować.

(ponownie komentarz), aby uzyskać więcej informacji na temat elastycznego sortowania, zobacz tutaj .

Marc Gravell
źródło
Tak ... to tylko kwestia pierwszego poziomu, używana do generowania linku do kolumny sortującej. na przykład. Jeśli mam model i chcę wyświetlić nazwę kolumny do posortowania, mogę użyć silnie wpisanego linku do obiektu, aby uzyskać nazwę właściwości, dla której dynamiczny linq nie ma krowy. Twoje zdrowie.
Schotime
ToStringpowinno dawać brzydkie wyniki dla wyrażeń jednoargumentowych.
nawfal
3

Utworzyłem metodę rozszerzenia na ObjectStateEntry, aby móc oflagować właściwości (klas Entity Framework POCO) zmodyfikowane w sposób bezpieczny dla typu, ponieważ metoda domyślna akceptuje tylko ciąg znaków. Oto mój sposób na uzyskanie nazwy z nieruchomości:

public static void SetModifiedProperty<T>(this System.Data.Objects.ObjectStateEntry state, Expression<Func<T>> action)
{
    var body = (MemberExpression)action.Body;
    string propertyName = body.Member.Name;

    state.SetModifiedProperty(propertyName);
}
Anders
źródło
3

Wykonałem INotifyPropertyChangedimplementację podobną do metody poniżej. Tutaj właściwości są przechowywane w słowniku w klasie podstawowej pokazanej poniżej. Oczywiście nie zawsze pożądane jest dziedziczenie, ale myślę, że w przypadku modeli widoku jest to dopuszczalne i daje bardzo czyste referencje właściwości w klasach modeli widoku.

public class PhotoDetailsViewModel
    : PropertyChangedNotifierBase<PhotoDetailsViewModel>
{
    public bool IsLoading
    {
        get { return GetValue(x => x.IsLoading); }
        set { SetPropertyValue(x => x.IsLoading, value); }
    }

    public string PendingOperation
    {
        get { return GetValue(x => x.PendingOperation); }
        set { SetPropertyValue(x => x.PendingOperation, value); }
    }

    public PhotoViewModel Photo
    {
        get { return GetValue(x => x.Photo); }
        set { SetPropertyValue(x => x.Photo, value); }
    }
}

Nieco bardziej złożoną klasę podstawową pokazano poniżej. Obsługuje tłumaczenie z wyrażenia lambda na nazwę właściwości. Zauważ, że właściwości są naprawdę pseudo właściwościami, ponieważ używane są tylko nazwy. Ale będzie wyglądać przezroczyście dla modelu widoku i odniesień do właściwości w modelu widoku.

public class PropertyChangedNotifierBase<T> : INotifyPropertyChanged
{
    readonly Dictionary<string, object> _properties = new Dictionary<string, object>();

    protected U GetValue<U>(Expression<Func<T, U>> property)
    {
        var propertyName = GetPropertyName(property);

        return GetValue<U>(propertyName);
    }

    private U GetValue<U>(string propertyName)
    {
        object value;

        if (!_properties.TryGetValue(propertyName, out value))
        {
            return default(U);
        }

        return (U)value;
    }

    protected void SetPropertyValue<U>(Expression<Func<T, U>> property, U value)
    {
        var propertyName = GetPropertyName(property);

        var oldValue = GetValue<U>(propertyName);

        if (Object.ReferenceEquals(oldValue, value))
        {
            return;
        }
        _properties[propertyName] = value;

        RaisePropertyChangedEvent(propertyName);
    }

    protected void RaisePropertyChangedEvent<U>(Expression<Func<T, U>> property)
    {
        var name = GetPropertyName(property);
        RaisePropertyChangedEvent(name);
    }

    protected void RaisePropertyChangedEvent(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private static string GetPropertyName<U>(Expression<Func<T, U>> property)
    {
        if (property == null)
        {
            throw new NullReferenceException("property");
        }

        var lambda = property as LambdaExpression;

        var memberAssignment = (MemberExpression) lambda.Body;
        return memberAssignment.Member.Name;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}
faester
źródło
1
Zasadniczo utrzymujesz torbę nieruchomości. Nieźle, ale te połączenia od pobierających i ustawiających klasy modelowe są trochę łatwiejsze public bool IsLoading { get { return GetValue(MethodBase.GetCurrentMethod().Name); } set { SetPropertyValue(MethodBase.GetCurrentMethod().Name, value); } }. Może być wolniejszy, ale bardziej ogólny i prosty.
nawfal
W rzeczywistości wdrożenie prostego systemu właściwości zależności jest trudniejsze (ale nie tak trudne), ale w rzeczywistości jest znacznie wydajniejsze niż powyższa implementacja.
Felix K.
3

To kolejna odpowiedź:

public static string GetPropertyName<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                      Expression<Func<TModel, TProperty>> expression)
    {
        var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

        return metaData.PropertyName;
    }
Notatka
źródło
1
ModelMetadataistnieje w System.Web.Mvcprzestrzeni nazw. Może nie pasuje do ogólnego przypadku
asakura89,
3

Opuszczam tę funkcję, jeśli chcesz uzyskać wiele pól:

/// <summary>
    /// Get properties separated by , (Ex: to invoke 'd => new { d.FirstName, d.LastName }')
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="exp"></param>
    /// <returns></returns>
    public static string GetFields<T>(Expression<Func<T, object>> exp)
    {
        MemberExpression body = exp.Body as MemberExpression;
        var fields = new List<string>();
        if (body == null)
        {
            NewExpression ubody = exp.Body as NewExpression;
            if (ubody != null)
                foreach (var arg in ubody.Arguments)
                {
                    fields.Add((arg as MemberExpression).Member.Name);
                }
        }

        return string.Join(",", fields);
    }
Carlos Bolivar
źródło
3
Wyjaśnisz to?
1

Oto inny sposób na uzyskanie właściwości PropertyInfo na podstawie tej odpowiedzi. Eliminuje to potrzebę wystąpienia obiektu.

/// <summary>
/// Get metadata of property referenced by expression. Type constrained.
/// </summary>
public static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
    return GetPropertyInfo((LambdaExpression) propertyLambda);
}

/// <summary>
/// Get metadata of property referenced by expression.
/// </summary>
public static PropertyInfo GetPropertyInfo(LambdaExpression propertyLambda)
{
    // /programming/671968/retrieving-property-name-from-lambda-expression
    MemberExpression member = propertyLambda.Body as MemberExpression;
    if (member == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));

    if(propertyLambda.Parameters.Count() == 0)
        throw new ArgumentException(String.Format(
            "Expression '{0}' does not have any parameters. A property expression needs to have at least 1 parameter.",
            propertyLambda.ToString()));

    var type = propertyLambda.Parameters[0].Type;
    if (type != propInfo.ReflectedType &&
        !type.IsSubclassOf(propInfo.ReflectedType))
        throw new ArgumentException(String.Format(
            "Expression '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));
    return propInfo;
}

Można to tak nazwać:

var propertyInfo = GetPropertyInfo((User u) => u.UserID);
Hans Vonn
źródło
1

Zaktualizowałem odpowiedź @ Camerona, aby uwzględnić pewne kontrole bezpieczeństwa w stosunku do Convertwpisywanych wyrażeń lambda:

PropertyInfo GetPropertyName<TSource, TProperty>(
Expression<Func<TSource, TProperty>> propertyLambda)
{
  var body = propertyLambda.Body;
  if (!(body is MemberExpression member)
    && !(body is UnaryExpression unary
      && (member = unary.Operand as MemberExpression) != null))
    throw new ArgumentException($"Expression '{propertyLambda}' " +
      "does not refer to a property.");

  if (!(member.Member is PropertyInfo propInfo))
    throw new ArgumentException($"Expression '{propertyLambda}' " +
      "refers to a field, not a property.");

  var type = typeof(TSource);
  if (!propInfo.DeclaringType.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
    throw new ArgumentException($"Expresion '{propertyLambda}' " + 
      "refers to a property that is not from type '{type}'.");

  return propInfo;
}
Shimmy Weitzhandler
źródło
1

Począwszy od .NET 4.0 można użyć ExpressionVisitordo znalezienia właściwości:

class ExprVisitor : ExpressionVisitor {
    public bool IsFound { get; private set; }
    public string MemberName { get; private set; }
    public Type MemberType { get; private set; }
    protected override Expression VisitMember(MemberExpression node) {
        if (!IsFound && node.Member.MemberType == MemberTypes.Property) {
            IsFound = true;
            MemberName = node.Member.Name;
            MemberType = node.Type;
        }
        return base.VisitMember(node);
    }
}

Oto jak korzystasz z tego użytkownika:

var visitor = new ExprVisitor();
visitor.Visit(expr);
if (visitor.IsFound) {
    Console.WriteLine("First property in the expression tree: Name={0}, Type={1}", visitor.MemberName, visitor.MemberType.FullName);
} else {
    Console.WriteLine("No properties found.");
}
dasblinkenlight
źródło
1

To może być optymalne

public static string GetPropertyName<TResult>(Expression<Func<TResult>> expr)
{
    var memberAccess = expr.Body as MemberExpression;
    var propertyInfo = memberAccess?.Member as PropertyInfo;
    var propertyName = propertyInfo?.Name;

    return propertyName;
}
Lucian Popescu
źródło
0
static void Main(string[] args)
{
    var prop = GetPropertyInfo<MyDto>(_ => _.MyProperty);

    MyDto dto = new MyDto();
    dto.MyProperty = 666;

    var value = prop.GetValue(dto);
    // value == 666
}

class MyDto
{
    public int MyProperty { get; set; }
}

public static PropertyInfo GetPropertyInfo<TSource>(Expression<Func<TSource, object>> propertyLambda)
{
    Type type = typeof(TSource);

    var member = propertyLambda.Body as MemberExpression;
    if (member == null)
    {
        var unary = propertyLambda.Body as UnaryExpression;
        if (unary != null)
        {
            member = unary.Operand as MemberExpression;
        }
    }
    if (member == null)
    {
        throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));
    }

    var propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
    {
        throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));
    }

    if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType))
    {
        throw new ArgumentException(string.Format("Expression '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(), type));
    }

    return propInfo;
}
Stas BZ
źródło