fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
5 minut

Strongly-typed DisplayValue i DisplayMember / DataValueField i DataTextField


18.06.2009

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:  		// reference type -> no conversion needed to object type
 20:  		if (body is MemberExpression)
 21:  		{
 22:  			memberExpression = (MemberExpression)body;
 23:  		}
 24:  		// simple type => boxing/conversion needed to return object type
 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.

0 0 votes
Article Rating
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
T2
T2
14 years ago

Jak każdy post – super

bodziec
bodziec
14 years ago

rewelacja, dzięki

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również