Potřebovali jsme někdy zavolat internal metodu, nebo získat hodnotu privatního fieldu, nebo vytvořit instanci internal třídy a používat její vlastnosti, nebo... tak toto v .NET lze provádět pomoci reflection. Reflection slouží k získávání informací o jednotlivých typech / tříd, umožňuje zjistit jaké fields, properties nebo metody obsahují, a také je možné provést jejich volání a to bez ohledu na to, jakou na úrovni objektového programování mají tyto members viditelnost. Samozřejmě by jsme toho neměli využívat často, ale jsou situace, kdy se to může hodit. (Např. zjistíte že v objektu existuje přesně ta metoda co se chová na rozdíl od té viditelné tak jak potřebujete, ale bohužel je internál.)
Základní používání si ukážeme na následující příkladu. Jedná se o code behind k ASP.NET stránce, na které je použit control ChangePassword pro změnu hesla. Ten umožňuje volat změnu hesla přes Membership provider. Pokud ho ale chceme použít a změnu hesla si chceme provést sami (např. bez Membershipu), provedeme to v ChangingPassword, ve které standardní volání vyrušíme nastavením Cancel. Po vlastní změně hesla je ale ještě potřeba zavolat metodu PerformSuccessAction, nebo v případě chyby nastavit zprávu metodou SetFailureTextLabel, obě metody jsou v controlu ale privátní. Příklad ukazuje jejich volání právě pomoci reflection.
public partial class ChangePassword : System.Web.UI.Page
{
protected void ChangePasswordControl_ChangingPassword(object sender, System.Web.UI.WebControls.LoginCancelEventArgs e)
{
const int cMinRequiredPasswordLength = 8;
e.Cancel = true;
if (changePasswordControl.NewPassword.Length < cMinRequiredPasswordLength)
{
SetFailureText(string.Format("New passwords are required to be a minimum of {0} characters in length.", cMinRequiredPasswordLength));
return;
}
var r = new System.Text.RegularExpressions.Regex(@"(?=(.*\d){1,})(?=(.*[a-z]){1,})(?=(.*[A-Z]){1,})");
if (!r.IsMatch(changePasswordControl.NewPassword)) //Nové heslo je "slabé"
{
SetFailureText("New password must contains lower case also large uppercase also digit characters!");
return;
}
string userName = System.Web.HttpContext.Current.User.Identity.Name;
if (string.Equals(changePasswordControl.NewPassword.Trim(), userName, StringComparison.OrdinalIgnoreCase))
{
//Nové heslo nesmí být shodné s přihlašovacím jménem
SetFailureText("New password match to Login name!");
return;
}
DoChangePassword(changePasswordControl.NewPassword); //Vlastní změna hesla
PerformSuccessAction();
}
private void PerformSuccessAction()
{
//Call PerformSuccessAction
System.Reflection.MethodInfo method = typeof(System.Web.UI.WebControls.ChangePassword).GetMethod("PerformSuccessAction", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
method.Invoke(changePasswordControl, new object[] { null, null, null });
}
private void SetFailureText(string failureText)
{
//Call SetFailureTextLabel
System.Reflection.FieldInfo field = typeof(System.Web.UI.WebControls.ChangePassword).GetField("_changePasswordContainer", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
object changePasswordContainer = field.GetValue(changePasswordControl);
System.Reflection.MethodInfo method = typeof(System.Web.UI.WebControls.ChangePassword).GetMethod("SetFailureTextLabel", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
method.Invoke(changePasswordControl, new object[] { changePasswordContainer, failureText });
}
private void DoChangePassword(string NewPassword)
{
//TODO:
}
}
V naší metodě PerformSuccessAction se přes reflection volá privátní metoda controlu ChangePassword.
V něm je definována takto:
private void PerformSuccessAction(string email, string userName, string newPassword);
Operátorem typeof se získá Type controlu a na něj se voláním metody GetMethod získá objekt MethodInfo popisující vybranou metodu. Při volání se určí jméno metody (string hodnotou) a dále příznaky BindingFlags. Ty v tomto případě určují, že metoda je instanční (Instance) a že není viditelná (NonPublic). Vlastní zavolání metody pak provedeme voláním Invoke na získaném objektu MethodInfo. Zde se předá instance typu ChangeUserPassword (control na stránce) a pole parametrů metody (zde 3x null).
Druhá metoda SetFailureText podobným postupem nejprve získá z controlu hodnotu vnitřního private fieldu _changePasswordContainer (typu internal třídy ChangePasswordContainer) a poté na control zavolá metodu SetFailureTextLabel s tímto získaným objektem. K volání metody je opět použito volání Invoke, k získání fieldu je na typu použito volání GetField, kde se obdobně předá jméno a BindingFlags jako v případě volání GetMethod. Metoda typu GetField vrací objekt FieldInfo, který umožňuje získat hodnotu metodou GetValue, případně pro nastavení hodnoty slouží SetValue.
Celé si to zkusme sepsat do postupu o několika krocích:
- Získání typu
- Pokud se jedná o public type:
Použijeme volání typeof s parametrem typu, nebo pokud máme instanci daného typu, zavoláme na ní GetType().
- Pokud se jedná o internal nebo private typ:
Nejprve musíme získat assembly, ve které se tento typ nachází, k tomu můžeme využít volání System.Reflection.Assembly.GetAssembly, nebo pokud známe jiný typ v této assembly, který je veřejný, je jednodušší typeof(<typ>).Assembly.
Typ pak načteme voláním metody GetType na assembly, jako parametr se zde určuje string s názvem typu.
- Zavolání metody / získání členu (member)
- Přímo zavolání metody:
Provedeme volání InvokeMember, parametry jsou string názvu metody, BindingFlags a pole object[] parametrů metody.
- Načtení infa členu třídy:
K tomu slouží následující metody třídy Type
|
Metoda |
Typ získaného objektu |
Vlastnost: |
GetProperty |
PropertyInfo |
Field: |
GetField |
FieldInfo |
Metoda: |
GetMethod |
MethodInfo |
Konstruktor: |
GetConstructor |
ConstructorInfo |
Událost: |
GetEvent |
EventInfo |
Parametry volání jsou string s názvem člena a BindingFlags.
- Určení BindingFlags
V kroku 2 určíme příznaky BindingFlags takto:
- BindingFlags.NonPublic – pokud není člen viditelný (public) nebo
BindingFlags.Public – pokud je člen viditelný.
- BindingFlags.Instance – pokud je člen na objektu nebo
BindingFlags.Static – pokud je člen statický na třídě.
- Pří volání metody ještě navíc určíme BindingFlags.InvokeMethod.
- Volání nebo získání / nastavení hodnot
Pokud jsem v kroku 2 načetli příslušný info objekt provedeme volání:
Volání Invoke - metody, konstruktoru
- Invoke – volání metody nebo konstruktoru (na MethodInfo nebo ConstructorInfo)
Parametry jsou instance a pole parametrů metody / konstruktoru.
- GetValue – načtení hodnoty vlastnosti / fieldu (na PropertyInfo / FieldInfo)
Parametry jsou instance a u indexovaných vlastnosti pole indexů.
SetValue – nastavení hodnoty vlastnosti / fieldu
Parametry jsou instance, hodnota a u indexovaných vlastnosti pole indexů.
- AddEventHandler – registrace handleru události (na EventInfo)
RemoveEventHandler – odregistrování handleru události
Parametry jsou instance, delegat události.
Samozřejmě tento postup neobsahuje všechny možnosti, na Type a info objektech lze volat i další metody, ale tyto vypsané jsou nejčastější operace.
A dále si uvedeme ještě některé příklady:
Assembly versionControlsAssembly = typeof(WritableFileDirectory).Assembly;
//WorkspaceContext context = WorkspaceContext.Get(workspace)
var workspaceContextType = versionControlsAssembly.GetType("Microsoft.TeamFoundation.VersionControl.Controls.WorkspaceContext");
object workspaceContext = workspaceContextType.InvokeMember("Get", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Static, null, null, new object[] { workspace });
//var model = workspaceContext.Model
var property = workspaceContextType.GetProperty("Model", BindingFlags.NonPublic | BindingFlags.Instance);
object model = property.GetValue(workspaceContext, new object[0]);
//var browser = new DialogHatFolderBrowser(context.Model, this.m_initialPath, true))
var browserType = versionControlsAssembly.GetType("Microsoft.TeamFoundation.VersionControl.Controls.DialogHatFolderBrowser");
var browserConstructor = browserType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { versionControlsAssembly.GetType("Microsoft.TeamFoundation.VersionControl.Controls.Model"), typeof(string), typeof(bool) }, null);
Form browser = (Form)browserConstructor.Invoke(new object[] { model, initialPath, true });
V tomto příkladu je nejprve volána statická metoda Get internal třídy WorkspaceContext, ta vrací objekt tohoto typu. Na něm je dále voláno načtení hodnoty internal vlastnosti Model. Třetí část příkladu načte typ DialogHatFolderBrowser a vytáhne jeho konstruktor. Protože konstruktorů má tato interní třída více variant, je zde ve volání GetConstructor navíc určeno jaké jsou typy parametrů konstruktoru (Model, string, bool). Poté je tento konstruktor zavolán pomoci Invoke, kde je jako první parametr předán objekt model vytažený v druhé části kódu.
Pozn. Vytvoření instance třídy lze také provést pomoci volání System.Activator.CreateInstance.
Ještě závěrem ukázka zaregistrování události:
Assembly versionControlsAssembly = typeof(WritableFileDirectory).Assembly;
var listViewExplorerType = versionControlsAssembly.GetType("Microsoft.TeamFoundation.VersionControl.Controls.ListViewExplorer");
var afterListFilledEvent = listViewExplorerType.GetEvent("AfterListFilled");
afterListFilledEvent.AddEventHandler(listViewExplorer, new EventHandler(ListViewExplorer_AfterListFilled));
Reflection umožňuje dostat se k věcem, které nejsou normálně určeny k používání. To dělá Reflection docela mocný nástroj, ale je jej potřeba používat s rozmyslem. Je zde totiž jedno velké nebezpečí, pokud např. přejdeme na novější verzi některé knihovny (ať už přímo z .NET nebo jiné) ze které něco pomoci reflection dolujeme, nikdo nám nezaručí, že používané internal / privátní metody / property / fields budou v nové verzi zachovány stejně. Navíc se chybu nedozvíme při kompilaci, ale až v runtime, nejčastěji tak, že některé MethodInfo / FieldInfo atd. budou mít hodnotu null.