Pseudo-style dla Windows Forms z Autofac

0

Zwykle aplikacja potrzebuje spójnego UI – czyli kontrolek wyglądających i zachowujących się wg. ustalonego schematu. W programowaniu web mamy style css, asp.net ma mechanizm Control Adapters, WPF z tego co wiem także pozwala dość mocno ustandaryzować ten aspekt. W Windows Forms komercyjne pakiety kontrolek, jak np. Telerik, udostępniają możliwość swego rodzaju stylowania wyglądu kontrolek danego typu.

Ale co z "gołym" WinForms? Przykład bardzo banalny: chciałbym, aby każdy ComboBox był renderowany z właściwością DropDownStyle=ComboBoxStyle.DropDownList zamiast domyślnego ComboBoxStyle.DropDown. Albo grid: nie chcę, aby którykolwiek z nich generował dodatkowy pusty wiersz pozwalający na dodanie nowej pozycji, nie chcę również, aby była możliwa edycja komórek. Do tej pory osiągałem takie coś albo przez dodatkowe kilka kliknięć w designerze w każdej formie, albo przez kopiuj/wklej odpowiednio "skonfigurowanej" kontrolki. Nie było to bardzo męczące, ale mimo wszystko wydaje się głupie. Jak wspomniałem niedawno, postanowiłem wyeliminować takie powtarzalne pierdoły ze swojego życia.

Jak osiągnąłem automatyzację całego procesu i zgrupowanie wszystkich zmian w jednym miejscu? Na początku zdefiniowałem bazowy interfejs i bazową klasę abstrakcyjną dla wszystkich takich "zmian", które chciałbym zaaplikować każdej kontrolce danego typu:

  1:  public interface ITheme
  2:  {
  3:  	void Apply(Control control);
  4:  }
  5:  
  6:  public abstract class ThemeBase<T> : ITheme
  7:  	where T : Control
  8:  {
  9:  	void ITheme.Apply(Control control)
 10:  	{
 11:  		if (CanProcess(control))
 12:  			Apply((T)control);
 13:  	}
 14:  
 15:  	private bool CanProcess(Control control)
 16:  	{
 17:  		return control is T;
 18:  	}
 19:  
 20:  	protected abstract void Apply(T control);
 21:  }

Dla każdego typu kontrolki tworzę klasę mającą za zadanie jej modyfikację. Przykładowy theme dla DataGridView wygląda w moim przypadku następująco:

  1:  public class DataGridViewTheme : ThemeBase<DataGridView>
  2:  {
  3:  	protected override void Apply(DataGridView control)
  4:  	{
  5:  		control.AllowUserToAddRows = false;
  6:  		control.AllowUserToDeleteRows = false;
  7:  		control.EditMode = DataGridViewEditMode.EditProgrammatically;
  8:  		control.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
  9:  		control.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
 10:  	}
 11:  }

Banał.

Dobra, a teraz… jak sprawić, aby przedstawione instrukcje zostały wykonane? Możliwość tą uzyskałem dzięki opisywanemu mechanizmowi Application Events. Przed pokazaniem jakiegokolwiek ekranu czy formy publikuję zdarzenie LoadingView:

  1:  public void Load<T>() where T : IMyView
  2:  {
  3:  	T view = ResolveView<T>();
  4:  
  5:  	AppEventsManager.Raise(new LoadingView(view));
  6:  
  7:  	//... actually showing the view

(więcej na ten temat – zarządzania widokami w architekturze MVP z Autofac – już wkrótce:) )

Zdarzenie to jest następnie obsługiwane przez odpowiedni handler:

  1:  public class ApplyThemes : Handles<LoadingView>
  2:  {
  3:  	private readonly IEnumerable<ITheme> _themes;
  4:  
  5:  	public ApplyThemes(IEnumerable<ITheme> themes)
  6:  	{
  7:  		_themes = themes;
  8:  	}
  9:  
 10:  	void Handles<LoadingView>.Handle(LoadingView e)
 11:  	{
 12:  		Handle(e.View as Control);
 13:  	}
 14:  
 15:  	private void Handle(Control control)
 16:  	{
 17:  		foreach (var ctl in control.AllChildControlsWithSelf())
 18:  		{
 19:  			foreach (var theme in _themes)
 20:  			{
 21:  				theme.Apply(ctl);
 22:  			}
 23:  		}
 24:  	}
 25:  }

Działanie bardzo proste: po utworzeniu, ale przed pokazaniem któregokolwiek widoku pobierana jest z niego cała hierarchia kontrolek i każda z nich jest modyfikowana przez themes, które potrafią ją obsłużyć.

Zwracam uwagę na dwie rzeczy. Pierwsza to metoda AllChildControlsWithSelf(), o której pisałem już dość dawno temu w poście "C# Power ponownie – Control.AllChildControls" (tam też implementacja). Druga to konstruktor handlera – dzięki parametrowi IEnumerable<ITheme> każda instancja automatycznie otrzyma wszystkie zarejestrowane w kontenerze Autofac themes.

No właśnie, "zarejestrowane"… Zgodnie z mechanizmem zaprezentowanym w notce "Samobudująca się aplikacja z Autofac" tworzymy moduł wykonujący to bardzo proste zadanie (w tym przypadku wszystko rejestruję jako singletony):

  1:  public class ThemesRegistrationModule : Module
  2:  {
  3:  	protected override void Load(ContainerBuilder builder)
  4:  	{
  5:  		builder
  6:  			.RegisterAssemblyTypes(AppDomain.CurrentDomain.GetAssemblies())
  7:  			.AssignableTo<ITheme>()
  8:  			.SingleInstance()
  9:  			.As<ITheme>();
 10:  	}
 11:  }

I to tyle. Od teraz zaaplikowanie zmian do wszystkich kontrolek danego typu wiąże się z napisaniem kilkulinijkowej, banalnej klaski. Czyż nie cool?

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

Comments are closed.