Jeśli w środowisku programistycznym .NET VisualStudio zdażyło się wam kiedyś projektować własne webowe kontrolki być może zauważyliście że w designerze jaki jest wbudowany w VisualStudio te własne kontrolki w czasie projektowania nie zachowują się tak jak powinny czyli zgodnie z logiką działania w runtime.
Designer jest ciekawie pomyślanym udogodnieniem ale rządzą nim trochę inne reguły niż standardową przeglądarką internetową dodatkowo nie jest w pełni zgodny z działaniem przeglądarki IE oraz nie działają w nim kody javascript co powoduje że dynamiczne zachowanie kontrolki w designerze może odbiegać od starannie zaprojektowanej logiki działania którą można obserwować w aplikacji.
Zwykle nie stanowi to strasznie wielkiego problemu w sytuacji gdy aplikacja jest niewielka i działanie kontrolek jest dobrze znane autorowi który ich używa w formatkach. Sytuacja jednak się komplikuje przy dużych systemach gdzie kontrolki są używane wielokrotnie przez programistów którzy tylko projektują formatki a nie pisali logiki działania kontrolek.
Było by dobrze żeby kontrolka w trakcie projektowania swoim zachowaniem i wyglądem naśladowała zachowanie z "runtime". Można oczywiście w funkcji generującej obraz html kontrolki (RenderContents lub Render) zrobić tak:
if (this.DesignMode)
{
//tworzenie kodu html dla trybu design
}
else
{
//tworzenie kodu html dla trybu runtime
}
I też się da zadbać o różny wygląd kontrolki w czasie działania programu i w czasie projektowania formatki. Jednak jest lepsze rozwiązanie umożliwiające zarówno rozdzielenie kodów dla tych dwóch trybów działania ale również dzięki temu będziemy mieli większe (znacznie) możliwości wpłynięcia zarówno na wygląd kontrolki w trybie design jak i na jej dynamiczne zachowanie.
Wystarczy przed kodem klasy naszej kontrolki dodać atrybut "Designer" definiiujący która klasa będzie obsługiwała tryb design tej kontrolki, może to np. wyglądać tak.
[Designer(typeof(myControlDesigner))]
public class MyControl : WebControl
{
}
Jak widać nasza kontrolka o nazwie MyControl dziedziczy sobie po klasie "WebControl" natomiast atrybut "Designer" określa że zachowaniem kontrolki w trybie projektowania będzie zarządzała klasa "myControlDesigner" która zaczyna się tak:
public class myControlDesigner : ControlDesigner
{
}
Jak widać nasza klasa Designera w tym wypadku dziedziczy po klasie "ControlDesigner" która to klasa jest standardowa klasą wywoływaną przez VisualStudio do obsługi prostych jednoelementowych kontrolek pochodzących od wspólnej dla nich klasy "System.Web.UI.Control". Jeśli sami nie zdefiniujemy naszej własnej klasy obsługującej tryb "Design" to ta właśnie klasa jest używana do stworzenia obrazu kontrolki gdy przełączamy w VS widok z kodu na tryb "Design"
W tym tutorialu będę opisywał modyfikację trybu design dla kontrolek Web ale w dosyć podobny sposób robi się to dla kontrolek WinFormsowych.
Jak już napisałem jeśli sami w atrybucie "Design" nie zdefiniujemy tego której klasy Designer ma użyć do zbudowania widoku kontrolki w trybie "Design" to on sam wtedy do obsługi kontrolki wybierze klasę najbardziej mu pasującą i tak np. jeśli będzie miał kontrolkę typu System.Web.UI.WebControls.Panel (otaczającą inne kontrolki) to najwłaściwszą klasą do obsługi trybu design dla tej kontrolki będzie klasa System.Web.UI.Design.WebControls.PanelContainerDesigner, a jeśli będzie to kontrolka użytkownika UserControl (składającą się z kilku innych kontrolek) najwłaściwszą klasą trybu design będzie w tym przypadku klasa System.Web.UI.Design.UserControlDesigner, projektowana do obsługi kontrolek złożonych zawierających w sobie inne kontrolki itd. itd..
Analogicznie jeśli sami projektujemy własną kontrolkę musimy się chwilę zastanowić która z klas designera (z przestrzeni nazw System.Web.UI.Design.*) najlepiej opisuje naszą kontrolkę i po tej klasie właśnie zdzidziczyć zachowanie w naszej klasie designera.
2.Rysowanie kontrolki w runtime.
Mająć już przygotowaną kontrolkę działającą w trybie "runtime" z określoną logiką i wyglądem chcielibyśmy aby działała ona i wyglądała przynajmniej podobnie w trybie design. Tak jak kontrolka dla trybu "runtime" ma przykrytą metodę "RenderContents" (lub "Render" jeśli nie chcemy mieć kontrolki otoczonej elementem span) która to metoda decyduje o tym jak ma ta kontrolka wyglądać w "runtime" tak samo istnieje analogiczna metoda dla naszej klasy designera którą należy przykryć aby móc przedefiniować wygląd i zachowanie kontrolki i "DesignMode". Tą metodą jest metoda "GetDesignTimeHtml". Metoda ta ma po prostu zwrócić stringa zawierającego cały kod html tworzący kontrolkę.
Oczywiście bardzo często może się okazać że zarówno w "runtime" jak i w "DesignMode" kod html będzie dosyć podobny a będzie tylko różnił się pewnymi szczegółami specyficznymi akurat dla przeglądarki lub dla designera wbudowanego w VisualStudio. Dość przyjemną właściwością designera z VS jest to że widzi wszystkie zdefiniowane w projekcie pliki szablonów wyglądu (css) co bardzo ułatwia zachowanie jednolitego wyglądu pomiędzy środowiskiem pracy a projektowania. Niestety ponieważ w designerze nie działają skrypty języka javascript dlatego jeśli jakaś część zachowania kontrolek zależy od dynamicznych kodów wykonywanych po stronie przeglądarki to zachowanie podobnego działania po stronie "designera" może wymagać utrzymywanie specjalnych szablonów dla trybu projektowania (co odradzam) albo można w tym celu wykorzystywać atrybut style lub class.
Wszystko właściwie zależy tylko od tego w jaki sposób jest zbudowana kontrolka i może się nawet okazać że wystarczy po prostu pobrać obraz html controlki z runtime jak i może się okazać w przypadku wielu operacji wykonywanych dynamicznie przez Javascript (podmienianie styli i klas) że zasymulowanie podobności wyglądu i działania będzie wymagało sporo pracy.
Przykładowo poniżej widzimy kod tworzący jakąś prostą kontrolkę
Jak widać kod ten jest prosty i jedynym poleceniem sterowania przepływu jest "if" sprawdzający stan właściwośći "Text" i w zależności od jego wypełnienia ustawia właściwą klasę "css" do zarządzania kolorem kontrolki.
I wszystko jest ok i w designerze spokojnie mogli byśmy wykorzystać html pobierany z kontrolki z jej metody "Render". Ale teraz wyobraźmy sobie to że nasza aplikacja przykładowo nie jest aplikacją wykorzystującą PostBacki a jest aplikacją WEB 2.0 korzystającą z AJAXa i w takim przypadku metoda do renderowania kontrolki tylko raz zostanie wykonana przy starcie strony a wszelkie dalsze zmiany w wyglądzie i zachowaniu strony zapewnią metody dynamicznego JavaScriptu po stronie przeglądarki.
W naszym prościutkim przykładzie będzie to np. dodanie do aplikacji następującego przykładowego kodu w javascript.
var onChange = function (par1)
{
if (par1.value.length <= 0)
par1.setAttribute("class", "empty");
else
par1.setAttribute("class", "notempty");
}
Czyli po stronie przeglądarki będziemy dynamicznie sprawdzać czy po zmianie wartości w kontrolce mają się zmienić klasy opisujące jej wygląd.
Jak od razu widać właściwie stosowanie "IFa" z metody "RenderContents" przestaje mieć sens ponieważ bez "PostBacków" prezentowana funkcjonalność będzie zawsze wywołana tylko raz i nie będzie w tym żadnego dynamizmu i interakcji z użytkownikiem.
Dynamizm zapewnia JavaScript który w przypadku wbudowanego w Visual Studio designera nie działa (może kiedyś będzie działał a może i nie).
W takim wypadku aby zepewnić pewną symulację dynamicznego działania wpływu zmiany właściwości "Text" na wygląd tego co widzimy w designerze, Designerowa metoda opisująca generację kodu html kontrolki mogła by np. wyglądać tak:
I już, po każdej zmianie wartości propertisa kontrolki silnik designera wywołuje virtualną metodę (tak tak można ją przedefiniować) "OnComponentChanged" która to z koleji gdzieś w swoim ciele woła metodę "UpdateDesignTimeHtml" która wołając metodę "GetDesignTimeHtml" (tą której domyślne działanie właśnie przykryliśmy) powoduje ponowne przerysowanie wyglądu twojej kontrolki czyli po prostu jej kod html zostanie stworzony od nowa i od nowa zostenie zaktualizowany odpowiedni "Tag" opisujący twoją kontrolkę a więc i jej obraz wizualny zostanie zmieniony (UFF). Krótko mówiąc po każdej zmienie jakiejś właściwości kod html opisujący kontrolkę jest generowany od nowa i obraz kontrolki w designerze jest odświeżany.
Moglibyśmy również uzyskać podobny efekt zostawiając w spokoju metodę "GetDesignTimeHtml" a można po prostu przejąć metodę designera "OnComponentChanged" która to jest zawsze wołana w momęcie zmiany wartości jakiegokolwiek property i w niej w zależności od odczytanej wartości property "Text" odpowiednio modyfikować właściwość designera o nazwie "Tag" (odzidziczyliśmy ją razem z klasą "ControlDesigner") a która to właściwość odzwierciedla nam programistyczny obraz tego co mamy w pliku aspx. Przykładowy kod takie działanie realizujący mógłby wyglądać np. tak:
W razie takiego podejścia pamiętajmy jednak że właściwość "Tag" reprezentuje nam kontekst kontrolki czyli właściwości odziedziczone z kontrolki "WebControl" (albo mówiąc inaczej to co możemy w tagu kontrolki ustawić w pliku aspx) a nie obraz HTML kontrolki wysyłany do przeglądarki.
Warte odnotowania jest również to że jeśli właśnie zmienianym property jest to którego działanie chcemy przedefiniować to nie możemy pozwolić aby wywołana została bazowa metoda "OnComponentChanged" ponieważ przywróći wtedy domyślną (poprzednią) wartość obrazu Taga w pliku aspx. Zamiast tego musimy tylko wywołać metodę UpdateDesignTimeHtml aby uaktualnić kod HTML w designerze. Jednak dla pozostałych property które zmieniamy musimy wywołać podstawową metodę z klasy bazowaj ponieważ jeśli tego nie zrobimy to wartości propertisów które zmieniamy nie będą przepisywane do pliku aspx (czasem właśnie o to nam może chodzić). Przedstawiona powyżej metoda modelowania zachowania kontrolki wydaje się nawet bardziej dynamiczna i ok niż metoda odrysowywania za każdym razem kontrolko od nowa, można w skrócie powiedzieć że metoda odrysowująca przypomina działanie bliskie "PostBack"om a metoda "eventowa" bardziej przypomina logikę dynamicznego HTMLa co kto woli i czego potrzebuje, można również dowolnie mieszać obie te metody i myślę do dopuki się nie pogubimy jest ok. Powyższa metoda jest również pomocna jeśli zależy nam po prostu na utrzymywaniiu pliku aspx w odpowiednim porządku tak aby niestandardowe ustawienia kontrolki miały swoje odzwierciedlenie w pliku aspx.
3.Ukrywanie wybranych właściwości kontrolki w property window
Standardowo wszystkie publiczne właściwości kontrolek i ich zdażenia (eventy) są wyświetlane w oknie właściwości (property window). Oczywiście nie zawsze jest to nam na rękę ponieważ może chcielibyśmy niektórych nie pokazywać w tym oknie pomimo że są one dostępne (publiczne) z poziomu kodu. Okazuje się że można zarządać tym zachowaniem designera na dwa sposoby. Prostszy z nich polega na dodaniu do klasy kontrolki atrybutu "Browsable(false)" i od tej chwili property występujący po tej deklaracji nie będzie już więcej widoczny w oknie właściwości. Metoda prosta i skuteczna ale oczywiście mieszamy wtedy kod odpowiedzialny za kontrolkę trybu "runtime" z zachowaniem harakterystycznym dla "DesignMode", ale istnieje inna (lepsza, a w każdym bądź razie bardziej skomplikowana metoda). Okazuje się bowiem że nasza klasa dziedzicząca po klasie ControlDesigner dostała za darmo cztery do tego wielce przydatne metody a mianowicie:
PreFilterProperties(IDictionary properties) - metoda wywoływana przed utworzeniem listy właściwości,
PostFilterProperties(IDictionary properties) - metoda wywoływana po utworzeniu listy właściwości,
PreFilterEvents(IDictionary events) - metoda wywoływana przed utworzeniem listy zdarzeń (events),
PostFilterEvents(IDictionary events) - metoda wywoływana po utworzeniu listy zdarzeń (events).
Wszystkie te metody są wirtualne dlatego bez problemu je przykryjemy we własnej klasie oraz wszystkie te metody w parametrze dostają listę właściwości którymi możemy sobie manipulować. Elementy na liście możemy sobie dodawać, usuwać i edytować a będzie to miało odzwierciedlenie w wizualnej liście właściwości i zdarzeń wyświetlanej w "property Window).
Są po dwie rodzaje metod dla właściwości i zdarzeń wersja "Pre" wywoływna przed utworzeniem listy i metody "Post" wywoływane już po utworzeniu listy. Metody "Pre" bardziej się nadają do dodawania i usuwania właściwości i zdażeń a metody "Post" bardziej do modyfikacji istaniejących już właściwości. Modyfikacja właściwości głównie polega na przypisywaniu nowych atrybutów do konkretnych pozycji listy właściwości i zdarzeń reprezentowanych przez klasy "PropertyDescriptor" i "EventDescriptor".
Poniżej przykłady pokazujący sposób dopisania atrybutu Browsable do określonych właściwości naszej klasy kontrolki:
protected override void PostFilterProperties(IDictionary properties)
{
PropertyDescriptor pd;
string[] noBrowseProperties = new string[] {
"Enabled",
"Height",
"Width",
"Visible"
};
for (int i = 0; i < noBrowseProperties.Length; i++)
{
pd = properties[noBrowseProperties[i]] as PropertyDescriptor;
if (pd != null)
{
properties[pd.Name] = TypeDescriptor.CreateProperty(pd.ComponentType, pd, new Attribute[2] { new BrowsableAttribute(false), new EditorBrowsableAttribute(EditorBrowsableState.Never) });
}
}
}
Jak widać do konkretnych właściwości dopisać można dowolne atrybuty (np. ich listę) dzięki czamu można dosyć dowolnie zmodyfikować działanie danej właściwości naszej kontrolki. W tym przypadku wybrane właściwości zostaną po prostu w VS ukryte.
Podobnie można postąpić ze zdarzeniami "Events" aby ukryć np. te których w naszej kontrolce nie wykorzystujemy.
protected override void PostFilterEvents(IDictionary events)
{
EventDescriptor evnt;
string[] noBrowseEvents = new string[] {
"DataBinding",
"Disposed",
"Init"
};
for (int i = 0; i < noBrowseEvents.Length; i++)
{
evnt = (EventDescriptor)events[noBrowseEvents[i]];
if (evnt != null)
{
events[noBrowseEvents[i]] = TypeDescriptor.CreateEvent(evnt.ComponentType, evnt, BrowsableAttribute.No);
}
}
base.PostFilterEvents(events);
}
4.Dodanie do property window nie istniejących w kontrolce właściwości.
Ciekawszą jednak możliwością jest możliwość dodania właściwości, lub zdarzeń które w naszej klasie kontrolki nie występują. Po co to komu można zapytać bo wydaje się to mało logiczne i przydatne. A no np. po to aby móc wywołać jakąś formatkę której ustawienia będą operować na więcej niż jednym polu lub będą wykonywać jakieś określone działania na całej kontrolce tak jak w poniższym przykładzie:
protected override void PreFilterProperties(IDictionary properties)
{
base.PreFilterProperties(properties);
properties["Align"] = TypeDescriptor.CreateProperty(
this.GetType(), // the type this property is defined on
"Align", // the name of the property
typeof(controlAlignment), // the type of the property
new Attribute[] { new CategoryAttribute("Design"), new EditorAttribute(typeof(ControlAlignUIEditor), typeof(System.Drawing.Design.UITypeEditor)) }); // attributes
}
Gdzie do kontrolki dodano sztuczną właściwość "Align" która będzie kontrolować położenie wybranych w designerze kontrolek. Nie zagłębiając się jednak zbyt głęboko w ten akurat przykład (miałem coś takiego w jednym z moich projektów) widać że do okna właściwości dodano pozycję "Align" która w sekcji wartości będzie miała formatkę reprezentowaną przez edytor typów właściwości opisany "custom"ową klasą "ControlAlignUIEditor" dziedziczącą zresztą po klasie "UITypeEditor". To że do właściwości jest dodany edytor widać przez to że tworząc właściwość "Align" dodaliśmy do niej dwa atrybuty, jeden to atrybut "Category" opusujący kategorię w której będzie na liście właściwości występowała dana właściwość (nie jest to wymagene przez edytor ale ładnie wygląda), ale drugim ważniejszym atrybutem jest atrybut "EditorAttribute" dzięki któremu możemy opisać jaka klasa (w tym wypadku formatka winForm) opisuje to co zobaczymy po wybraniu wartości właściwości "Align". Oczywiście dowolny edytor możemy dodać (przez atrybut EditorAttribute) do już jakiejść istniejącej właściwości a jedynie w tym przypadku został wykorzystany do zobrazowania faktu że do listy właściwości kontrolki można dodać zupełnie nową obcą właściwość nie występującą we właściwościach danej kontrolki. W ten sposób możemy zaprogramować całkiem sporą funkcjonalność (zaszytą w designMode) wykorzystaywaną tylko na etapie projektowania jakiejś formatki.
5.Dodanie do property window nie istniejącego w kontrolce zdarzenia (event).
Tak samo jak można dodać do listy zdarzeń kontrolki nie istniejącą właściwość, podobnie można dodać zdażenie które fizycznie nie istnieje w kontrolce. Czyli uszczegółowiając nie ma w kontrolce wpisu o zdarzeniu np. nie ma czegoś takiego "public event EventHandler Click;". Normalnie takie istniejące zdarzenie wyświetla się na liscie zdarzeń kotnrolki w designerze (property window), ale jeśli z różnych powodów nie mogliśmy do kontrolki dodać zdażeń ponieważ np. w naszym systemie to nie kontrolki przechowują swoje zdarzenia ale np. zdarzenia są przechowywane w jakimś centralnym miejscu zarządzanym np. przez jakiegoś managera zdarzeń a my potrzebujemy zarządzać zdarzeniami z poziomu designera to dodanie takiego sztucznego zdażenia można zrobić np. tak.
protected override void PreFilterEvents(IDictionary events)
{
EventDescriptor ed_click = TypeDescriptor.CreateEvent(typeof(MyControl), "Click", typeof(ActionFunc), new DesignOnlyAttribute(true), new BrowsableAttribute(true), new MergablePropertyAttribute(false));
CustomEventDescriptor ced_click = new CustomEventDescriptor(ed_click, typeof(ActionFunc));
events.Add("Click", ced_click);
base.PreFilterEvents(events);
}
Niestety występuje tu pewna niedogodność mianowicie podczas tworzenia przy pomocy metody "TypeDescriptor.CreateEvent" elementu klasy EventDescriptor okazuje się że utworzony obiekt jest wadliwy mianowicie jego właściwości "ComponentType" i "EventType" przy próbie odczytu generują wyjątek który powoduje że cała lista zdarzeń nie będzie widoczna na liście zdarzeń w designerze. Aby pominąć tę niedogodność zastosowałem dodatkową klasę CustomEventDescriptor która niejako pośredniczy pomiędzy etapem tworzenia eventu a atapem dodawania naszego nowego zdarzenia do listy zdarzeń "events.Add()". klasa "CustomEventDescriptor" powoduje że problemowe wartości są ustawiane właściwymi wartościami więc w czasie dodawania nowego zdarzenia do kolekcji zdarzeń wyjątki nie występują. Oto kod klasy "CustomEventDescriptor":
internal sealed class CustomEventDescriptor : EventDescriptor
{
private readonly Type eventTypeAlias;
private readonly Type componentType;
public CustomEventDescriptor(EventDescriptor descr, Type aType) : base(descr, null)
{
componentType = descr.ComponentType;
eventTypeAlias = aType;
}
public CustomEventDescriptor(EventDescriptor descr, Type aType, Type cType) : base(descr, null)
{
componentType = cType;
eventTypeAlias = aType;
}
public override Type ComponentType{get { return componentType; }}
public override Type EventType{get { return eventTypeAlias; }}
public override bool IsMulticast { get { return false; } }
public override void AddEventHandler(object component, Delegate value){}
public override void RemoveEventHandler(object component, Delegate value){}
}
Jak widać klasa ta niczego specjalnego nie robi poza ustawianiem właściwości componentType i eventTypeAlias które po wygenerowaniu pierwotnej klasy przez procedurę CreateEvent są nie ustawione i próby ich odczytania przez designera generują wyjątek powodujący nie wypełnienie całej listy wyjątków.
6. Własne zdarzenia w designerze oraz wykorzystanie serwisów
Skoro już pokazaliśmy że można dodać własne nie istniejące w kontrolce zdażenie do listy zdarzeń kontrolki w designerze to można się już np. przestać ograniczać domyślnym działaniem jakie designer robi w momencie dwuklika na danym zdażeniu. Otóż jak wiadomo jeśli nie mamy podpiętej do zdarzenia (eventu) żadnej procedury obsługi tego zdarzenia to po dwukliku w takie zdarzenie designer wygeneruje nam w naszym pliku *.aspx.cs domyślną procedurę do zdarzenia z nazwą składającą się z nazwy zdarzenia a do tagu w pliku *.aspx doda atrybut przypisujący to zdarzenie do elementu Tag opisującego naszą kontrolkę.
I tu możemy mieć pewien problem jeśli w naszym customowym systemie mamy zupełnie przepisaną obsługę zdarzeń i nie korzystamy z mechanizmów aspx tylko mamy zrobioną własną obsługę zdarzeń po stronie serwera i klienta. W takim wypadku po dwukliku na zdarzeniu powinniśmy wygenerować własną procedurę o naszej własnej nazwie a może również przydało by się coś jeszcze dodatkowego dopisywać do pliku *.aspx.cs np. jakieś procedury dodające event do naszego managera zdarzeń etc. W każdym bądź razie możemy nie chcieć aby designer generował za nas nazwę procedury bo chcemy mieć ją inną albo nawet możemy nie chcieć aby jakąkolwiek procedurę sam nam dopisywał do pliku *.aspx.cs bo chcemy to sami zrobić.
W takim wypadku możemy podmienić tzw. serwis o nazwie "IEventBindingService".
Tu muszę napisać pewne wyjaśnienie, w designerze działa bardzo dużo takich serwisów które tak na prawdę są klasami odpowiednich typów. Klasy te wykonują pewne określonego rodzaju usługi dla designera każda z tych klas odpowiada za swojego typu działanie i tak np. przykładowo klasa implementująca interfejs IEventBindingService odpowiada właśnie za wyświetlanie i obsługę listy zdarzeń wyświetlanych w "property window" są tam różne metody np. metoda generujące nazwę zdażenia o nazwie "CreateUniqueMethodName" jest tam metoda do pobierania listy zdażeń o nazwie "GetEventProperties" jest tam w końcu kilka metod o wspólnej nazwie "ShowCode" które służą do wygenerowania procedur danego zdarzenia które to będą zapisane do pliku *.aspx.cs. (Uważny czytelnik pewnie zauważy że dodanie eventa do listy eventów może być również zrealizowane przez podmienienie w designerze serwisu na naszą klasę i przykrycie w tej klasie metody "GetEventProperties" tak aby zwracała więcej eventów niż klasa bazowa).
Natomiast np. serwis implementujący interfejs "ISelectionService" odpowiada za operacje wykonywane w momęcie wybierania kontrolek i mając przejęty taki serwis możemy np. podczepić własną procedurę pod zdarzenie serwisu "SelectionChanged" dzięki czemu możemy wykonywać własne działania w momęcie wybierania kontrolek w designerze.
Bardzo przydatnym serwisem jest serwis opisany interfejsem "IComponentChangeService" Przejęcie tego serwisu i podpięcie się pod jego metodę "ComponentChanged" (jest również wersja sprzed zmiany czyli "ComponentChanging") umożliwi nam zarządzanie działaniami jakie designer wykonuje na kontrolkach w czasie zmiany jakiś właściwości kontrolki w oknie "Property window".
Czuję się w tym momęcie zobowiązany złożyć wyjaśnienie czym różni się metoda "ComponentChanged" serwisu "IComponentChangeService" od virtualnej metody "OnComponentChanged" odziedziczonej przez nas po klasie ControlDesigner. Różnica polega głównie na tym że klasa "OnComponentChanged" z designera jest wywoływana tylko na jednej kontrolce tej właśnie której właściwość (property) w "Windows property" zmieniamy. Natomiast klasa serwisu jest wywoływana dla wszystkich kontrolek jakie są w tym widoku i to do nas należy rozpoznanie czy zmienialiśmy tę właśnie kontrolkę której wywołanie klasy zdarzenia nastąpiło można to zrobić np. tak:
void ChangeService_ComponentChanged(object sender, ComponentChangedEventArgs e)
{
if (e.Component is MyControl && ((MyControl)e.Component).ID == ((MyControl)this.Component).ID)
{
if (e.Member.Name.Equals("Click"))
//costam
}
}
Jak widać ponieważ w tym akurat przypadku możemy nie chcieć wykonywać pewnych operacji na wszystkich kontrolkach (bo mogą np. nie mieć potrzebnych nam właściwości czy metod dlatego sprawdzamy czy na pewno komponent dla którego wywołano tę metodę "e.Component" pochodzi na pewno od klasy naszej kontrolki "e.Component is MyControl" a jeśli petrzebujemy już tylko znaleść wywołanie tej metody dla na pewno kontrolki której zmiany dotyczą można również porównać unikatowy identyfikator kontrolki dla krórej wywołano zdarzenie z podstawową kontrolką krórą obsługuje ta akurat klasa designera "((MyControl)e.Component).ID == ((MyControl)this.Component).ID".
Ciekawą (i przydatną) właściwością jest to że metoda "ChangeService_ComponentChanged" zostanie wywołana zarówno przy zmianie wartości właściwości (property) jak i przy zmianie zdarzenia (event) i przez rozpoznanie nazwy właściwości "if (e.Member.Name.Equals("CHANGE"))" musimy rozpoznać z czym mamy do czynienia.
Gdzieś to w tekście powyżej już się przewijało ale dla jasności napiszę że serwisy można właściwie wykorzystywać na dwa podstawowe sposoby.
Można pobrać taki serwis (oczywiście referencję do niego) przy pomocy metody designera "GetService" robi się to np. tak:
IComponentChangeService changeService = GetService(typeof(IComponentChangeService)) as IComponentChangeService;
A mając już adres serwisu można do jego eventu "ComponentChanged" przypisać własną metodę obsługi która właśnie się wywoła w momęcie zmieny właściwości w oknie "Properry Window" (ale na wszystkich kontrolkach więc jeśli potrzebujemy tylko zmian w jednej kontrolce zalecam używać metody OnComponentChanged designera)
Podpięcie własnej metody do serwisu może wyglądać np. tak:
if (this.changeService != null)
{
this.changeService.ComponentChanged += new ComponentChangedEventHandler(ChangeService_ComponentChanged);
}
A metoda przypięta do eventu ComponentChanged może wyglądać tak:
Drugim sposobem na wykorzystanie serwisów jest zupełne podmienienie całego serwisu na naszą klasę dziedziczącą po tym serwisie można to zrobić np. tak:
IEventBindingService eventBindingService = GetService(typeof(IEventBindingService)) as IEventBindingService;
if (this.eventBindingService != null)
{
Type type = typeof(IEventBindingService);
host.RemoveService(type);
host.AddService(type, new myEvBinServ(this.eventBindingService));
}
W tym wypadku podstawiamy własną klasę "myEvBinServ" która to klasa będzie odpowiadała za to aby domyślne metody dla zdażeń w pliku nie były generowane czyli wszystkie metody ShowCode mają zwracać wartość false;
public class myEvBinServ : IEventBindingService
{
private IEventBindingService _eventBindingService;
public myEvBinServ(IEventBindingService eventBindingService)
{
_eventBindingService = eventBindingService;
}
public String CreateUniqueMethodName(IComponent component, EventDescriptor e)
{
return String.Format("On{0}_{1}", ((MyControl)component).ID, e.Name);
}
public ICollection GetCompatibleMethods(EventDescriptor e)
{
ICollection col = _eventBindingService.GetCompatibleMethods(e);
return col;
}
public EventDescriptor GetEvent(PropertyDescriptor property)
{
return _eventBindingService.GetEvent(property);
}
public PropertyDescriptorCollection GetEventProperties(EventDescriptorCollection events)
{
return _eventBindingService.GetEventProperties(events);
}
public PropertyDescriptor GetEventProperty(EventDescriptor e)
{
return _eventBindingService.GetEventProperty(e);
}
public bool ShowCode()
{
return false;
//return _eventBindingService.ShowCode();
}
public bool ShowCode(int lineNumber)
{
return false;
//return _eventBindingService.ShowCode(lineNumber);
}
public bool ShowCode(IComponent component, EventDescriptor e)
{
return false;
//return _eventBindingService.ShowCode(component, e);
}
}
Do konstruktora klasy przekazujemy orginalny serwis po to aby w niektórych przypadkach móc wywoływać orginalny serwis przykryty naszą nakładką. Będzie tak np. w przypadku jeśli chcielibyśmy aby ciała metod w pliku *.aspx.cs były generowane a tylko zależało by nam na zmianie nazwy tych metod w takim wypedku trzeba by zrezygnować ze zwracania wartości false przez metody ShowCode i odkomentować wywołanie orginalnego serwisu. Jeśli tak zrobimy właściwe zastosowanie znajdzie również wtedy przykrycie przez nas metody "CreateUniqueMethodName" która generuje naszą nazwę metody, można w ten sposób zarządzać jej nazwą. Ale w przypadku jeśli w ogóle rezygnujemy z generacji ciał metod zdarzeń to i generacja nazwy nie będzie wykorzystywana i trzeba będzie samemu w innym miejscu tworzyć nazwę metody.
7. Generacja własnej procedury obsługi w pliku aspx.
Skoro juz w poprzednim przykładzie zablokowaliśmy generację metod zdarzeń kontrolki przez designer, ponieważ chcieliśmy mieć własne zupełnie inne metody dodane do pliku *.aspx.cs to teraz musimy jakoś sami oprogramować modyfikację pliku zdefinowanego w "codeBehind" (*.aspx.cs).
Prawdopodobnie dało by się to zrobić w tej naszej klasie implementującej interfejs "IEventBindingService" którą podmieniliśmy serwis w designerze prawdopodobnie trzeba by napisać własne procedury "ShowCode" jednak z uwagi że takie podejście mi po prostu nie wychodziło zadanie to zrealizowałem inaczej. Mianowicie po pobraniu serwisu "IComponentChangeService" i podpięciu się pod jego zdażenie "ComponentChanged" sprawdzam czy nastąpiła zmiana właściwośći zdażenia w "property Window" i jeśli tak to przy pomocy obiektu do automatyzacji Visual Studio "EnvDTE" dobieram się do odpowiedniego pliku z projektu dopisując do niego odpowiedni kod.
Oto moja metoda do przejmowania zmian we właściwościach.
private void ChangeService_ComponentChanged(object sender, ComponentChangedEventArgs e)
{
if (e.Component is MyControl && ((MyControl)e.Component).ID == ((MyControl)this.Component).ID)
{
if (e.Member.Name.Equals("Click"))
{
CreateMethodEventBody(e.Member.Name, ((MyControl)e.Component).ID, e.NewValue.ToString());
}
}
}
Jak widać jedyną nowością w porównaniu do poprzednio przedstawionych wersji tej metody jest wywołanie funkcji "CreateMethodEventBody" która to funkcja przy pomocy obiektu "EnvDTE" (do automatyzacji czynności w Visual Studio) modyfikuje odpowiedni plik projektu. Można to działanie napisać w dowolny sposób wybrany przez konkretnego programistę więc jeśli ktoś lubi parsowanie plików z kodami źródłowymi to może to robić zupełnie na strumieniach plikowych, ja wykorzystałem obiekt "EnvDTE" ponieważ obiekt ten umożliwia poruszanie się po strukturze kodu (w tym wypadku C#) jak po drzewie co jest niesamowitym ułatwieniem przy konstruowaniu takiego algorytmu. Oto kod to robiący:
protected void CreateMethodEventBody(string eventName, string ID, string methodName)
{
this.CreateMethodEventBody(eventName, vsCMTypeRef.vsCMTypeRefVoid, ID, methodName);
}
protected void CreateMethodEventBody(string eventName, vsCMTypeRef returnType, string ID, string methodName)
{
EventDescriptorCollection eventColl = TypeDescriptor.GetEvents(this.Component, new Attribute[0]);
PropertyDescriptor epd;
if (eventColl != null)
{
EventDescriptor ed = eventColl[eventName] as EventDescriptor;
if (ed != null)
{
//string methodName = this.eventBindingService.CreateUniqueMethodName(this.Component, ed);
epd = this.eventBindingService.GetEventProperty(ed);
EnvDTE.DTE dte = (EnvDTE.DTE)GetService(typeof(EnvDTE.DTE));
string name = dte.ActiveDocument.Name;
string path = dte.ActiveDocument.Path;
TextDocument objAspxDoc = dte.ActiveDocument.Object("TextDocument") as EnvDTE.TextDocument;
EditPoint objEP = objAspxDoc.StartPoint.CreateEditPoint();
EditPoint objEndEP = objAspxDoc.EndPoint.CreateEditPoint();
string calyAspx = objEP.GetText(objEndEP);
string codeBehind = string.Format("{0}.cs", dte.ActiveDocument.Name);
string[] nameParts = dte.ActiveDocument.Name.Split(new string[] { "." }, StringSplitOptions.None);
string inherits = nameParts.First();
try
{
string startTag = "<%@ Page";
string endTag = "%>";
int startIndex = calyAspx.IndexOf(startTag);
int endIndex = calyAspx.IndexOf(endTag, startIndex) + endTag.Length;
string contentAspx = calyAspx.Substring(startIndex, endIndex - startIndex);
contentAspx = contentAspx.Replace("%@ ", "");
contentAspx = contentAspx.Replace("%>", "/>");
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(contentAspx);
if (xmlDoc.FirstChild.NodeType == XmlNodeType.Element && xmlDoc.FirstChild.Name == "Page")
{
codeBehind = xmlDoc.FirstChild.Attributes["CodeBehind"].Value;
inherits = xmlDoc.FirstChild.Attributes["Inherits"].Value;
string[] inhParts = inherits.Split(new string[] { "." }, StringSplitOptions.None);
inherits = inhParts.Last();
}
}
catch (Exception) { }
//now we try to find item/file with codeBehind "aspx.cs"
ProjectItem csItem = null;
foreach (ProjectItem pitem in dte.ActiveDocument.ProjectItem.ProjectItems)
{
if (pitem.Name == codeBehind)
{
csItem = pitem;
break;
}
}
//FileCodeModel fileCM = csItem.FileCodeModel;
CodeElements ces = csItem.FileCodeModel.CodeElements;
CodeNamespace cns = findCodeElement(ces, "DesignerPresentation", vsCMElement.vsCMElementNamespace) as CodeNamespace;
if (cns == null)
return;
ces = cns.Members;
if (ces == null)
return;
CodeClass cls = findCodeElement(ces, inherits, vsCMElement.vsCMElementClass) as CodeClass;
if (cls == null)
return;
ces = cls.Members;
//point of code where we jump and show code
TextPoint tp = cls.GetEndPoint(vsCMPart.vsCMPartBody);
EditPoint ep = tp.CreateEditPoint();
CodeFunction cf = findCodeElement(ces, methodName, vsCMElement.vsCMElementFunction) as CodeFunction;
if (cf == null) //We add method only if it no body
{
cf = cls.AddFunction(methodName, vsCMFunction.vsCMFunctionFunction, returnType, -1, vsCMAccess.vsCMAccessPublic);
tp = cf.GetStartPoint(vsCMPart.vsCMPartBody);
ep = tp.CreateEditPoint();
ep.Indent();
ep.Insert("//Tu proszę wpisywać kod obsługi metody");
if (cf != null)
{
epd.SetValue(this.Component, methodName);
}
}
Window win = dte.ItemOperations.OpenFile(dte.ActiveDocument.Path + csItem.Name);
if (win != null)
{
TextDocument objTextDoc = dte.ActiveDocument.Object("TextDocument") as EnvDTE.TextDocument; //after openFile active document become cs file with code (previous active was aspx file)
objTextDoc.Selection.GotoLine(ep.Line);
}
}
}
}
private CodeElement findCodeElement(CodeElements ces, string elementName, vsCMElement elementType)
{
CodeElement ce = null;
foreach (CodeElement cec in ces)
{
if (cec.Kind == elementType)
if (cec.Name == elementName)
{
ce = cec as CodeElement;
break;
}
}
return ce;
}
Podałem ten kod jako przykład, jako jakąś możliwość do wzorowania się, oczywiste jest że każdy będzie musiał stworzyć tą procedurę dokładnie pod swój projekt tak aby działanie kodu dopisującego coś do pliku "CodeBehind" (*.aspx.cs) odzwierciedlało strukturę rozwiązania zastosowanego w danym projekcie.
Designer wbudowany w Visual Studio ma dosyć szerokie możliwości i opisanie ich w tym krótkim artykule było by tródne, tym bardziej tródne że ja w trakcie pracy nad projektem gdzie odkrywałem meandry Designera po prostu ich wszystkich nie odkryłem, wykorzystałem to co było mi akurat potrzebne dla realizacji postawionego przede mną zadania. Niektórych dosyć oczywistych rzeczy takich jak tworzenie podręcznych menu "ActionLists", "DesignerVerbCollection" czy tworzenia edytorów UITypeEditor nie opisywałem bo jest to dosyć intuicyjne a zresztą jest na to sporo przykładów w necie a raczej starałem się opisać to co mi sprawiało tródność a co nie jest zbyt standardową operacją designera (jak na przykład podmiana serwisu czy dodanie nieistniejącego eventa). Pozdrawim innych designerowych samurajów w ich trudnej i nierównej walce z designerem.
8 .Załączniki
Projekt w Visual Studio 2010 prezentujący prostą kontrolkę użytkownika i jej reprezentację w designerze Designer presentation