Ileż to razy zmuszeni jesteśmy pisać kod temu podobny:
1: list.ValueMember = "Id";
2: list.DisplayMember = "Name";
Na CodeGuru niejednokrotnie pytano o jakiś sposób na rozwiązanie tego problemu. Podawanie stringów jest ZŁE, niewygodne i bardzo podatne na błędy wszelakie. Zmiana nazwy właściwości rozwala UI, refactoring bez dodatkowych narzędzi jak Resharper potrafi napsuć sporo krwi (a i z pomocą R# wcale przyjemny nie jest)... Syf, kiła i mogiła.
Postanowiłem poeksperymentować w tym obszarze i oto co udało mi się osiągnąć. Mając klasę User:
1: public class User
2: {
3: public int Id { get; set; }
4: public string Name { get; set; }
5: }
Możemy zrobić coś takiego:
1: list.ValueMember<User>(u => u.Id);
2: list.DisplayMember<User>(u => u.Name);
Fajnie, prawda? Zobaczmy jak taka magia wygląda pod spodem.
Wykonanie tych instrukcji bez podawania nigdzie jawnie żadnego stringa możliwe jest dzięki LINQ, a dokładniej klasie Expression. O niej i wszystkim co się z nią wiąże napiszę być może kiedy indziej (a nawet jeśli nie to zachęcam do samodzielnego zapoznania się z tematem), dzisiaj natomiast ograniczę się do pokazania metod zademonstrowanych powyżej:
1: public static class ListControlExtensions
2: {
3: public static void ValueMember<T>(this ListControl _this, Expression<Func<T, object>> retrieve)
4: {
5: _this.ValueMember = GetMemberName(retrieve);
6: }
7:
8: public static void DisplayMember<T>(this ListControl _this, Expression<Func<T, object>> retrieve)
9: {
10: _this.DisplayMember = GetMemberName(retrieve);
11: }
12:
13: private static string GetMemberName<T>(Expression<Func<T, object>> retrieve)
14: {
15: Expression body = retrieve.Body;
16:
17: MemberExpression memberExpression;
18:
19:
20: if (body is MemberExpression)
21: {
22: memberExpression = (MemberExpression)body;
23: }
24:
25: else if (body.NodeType == ExpressionType.Convert && ((UnaryExpression)body).Operand is MemberExpression)
26: {
27: memberExpression = (MemberExpression)((UnaryExpression)body).Operand;
28: }
29: else
30: {
31: throw new ArgumentException(string.Format("This lambda: {0} is not supported!", retrieve.Body.ToString()));
32: }
33:
34: return BuildMemberPath(memberExpression);
35: }
36:
37: private static string BuildMemberPath(MemberExpression memberExpression)
38: {
39: string thisName = memberExpression.Member.Name;
40:
41: if (memberExpression.Expression is MemberExpression)
42: return BuildMemberPath((MemberExpression)memberExpression.Expression) + "." + thisName;
43: return thisName;
44: }
45: }
Jak widać kluczowym elementem jest metoda GetMemberName. Zwraca ona podane wyrażenie lambda w postaci tekstowej, w sam raz do przypisania do DisplayMember (lub, jeśli już przy tym jesteśmy, do DataTextField w kontekście aplikacji web).
Takie operacje sprawdzane w czasie kompilacji są nam udostępnione, ponieważ jako parametr tych metod przyjmujemy Expression<Func<...>> a nie samo Func<...>. Dzięki temu cała instrukcja rozbita na części pierwsze stoi przed nami (p)otworem. Logika przedstawionych operacji jest bardzo prosta:
1) sprawdzamy, czy całe wyrażenie jest pobraniem wartości jakiejś składowej klasy –> jeśli tak, budujemy z niego tekst
2) jeśli nie, sprawdzamy, czy mamy przypadkiem do czynienia z rzutowaniem jako główną operacją wyrażenia; do mechanizmu możemy przekazać wyrażenie zwracające object (dokładniej: Func<T, object>), więc w przypadku typów prostych (jak np. int) musi zajść tzw. boxing; w tym przypadku zakładamy, że "podrzędne" do rzutowania wyrażenie jest pobraniem wartości ze składowej klasy i na nim opieramy budowanie tekstu
3) jeżeli żaden z powyższych scenariuszy nie jest spełniony, wyrzucamy wyjątek, na przykład dla wywołania
1: list.ValueMember<User>(u => u.Id + 666);
otrzymamy komunikat:
Budowanie tekstu to prosta rekurencja wędrująca po wywołaniach kolejnych składowych i sklejająca poszczególne części. Dzięki temu wywołanie:
1: list.ValueMember<User>(u => u.UserAddress.AddressCity.Name);
wygeneruje tekst: "UserAddress.AddressCity.Name".
ALE HOLA HOLA! Oczywiście takie wywołanie na niewiele się zda w przedstawianym scenariuszu. Jak wiadomo, zarówno Windows Forms ze swoimi ValueMember i DisplayMember jak i ASP.NET z DataValueField i DataTextField nie wspierają wywoływania zagnieżdżonych właściwości. Dlatego też pomimo faktu, że mamy mechanizm potrafiący spisać dowolnie skomplikowany łańcuch zagnieżdżeń, i tak wykorzystać możemy jedynie jego najprostsze działanie w postaci u => u.Id czy u => u.Age. Ale w końcu lepsze to niż to co mieliśmy wcześniej.
Tak czy siak zachęcam to eksplorowania tej działki .NETa, ponieważ można natknąć się na naprawdę interesujące zastosowania eliminujące dręczące nas problemy.