Dziś kolejny post z cyklu zastosowanie słowa kluczowego dynamic. Ostatnio pisałem o ExpandoObjet, który jest dynamicznym kontenerem na metody i dane. DynamicObject pozwala z kolei tworzyć wrappery na różne klasy. Zacznijmy od przykładu:
internal class Program { public class CustomWrapper : DynamicObject { public override bool TryGetMember(GetMemberBinder binder, out object result) { result = "Hello World"; return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { return true; } } private static void Main(string[] args) { dynamic wrapper=new CustomWrapper(); Console.WriteLine(wrapper.AnyProperty);// Hello world wrapper.AnyProperty = "test"; Console.WriteLine(wrapper.AnyProperty);// Hello World } }
Zawsze dziedziczymy po DynamicObject – nie używamy tej klasy bezpośrednio.
Gdy próbujemy uzyskać dostęp do jakiegoś elementu w klasie DynamicObject, najpierw sprawdzane są normalne właściwości, zdefiniowane w klasie. Jeśli dana właściwość nie istnieje wtedy wywoływana jest metoda TryGetMember, która za pomocą result zwraca wynik. W powyższym przykładzie każde wywołanie nowej właściwości zwraca tekst Hello World. TrySetMember pełni funkcję analogiczną ale wywoływany jest w momencie ustawiania wartości. Napiszmy również wrapper dla klasy string, który potrafi wywoływać różne metody:
public class CustomWrapper : DynamicObject { private string _container = "Hello World"; public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result= typeof(string).InvokeMember( binder.Name, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, _container, args); return true; } } private static void Main(string[] args) { dynamic wrapper=new CustomWrapper(); Console.WriteLine(wrapper.ToUpper());// HELLO WORLD } }
Pamiętacie z poprzedniego postu ExpandoObject o XML to LINQ? Alexandra Rusina pokazuje na swoim blogu również jak zrobić to za pomocą DynamicObject:
public class DynamicXMLNode : DynamicObject { XElement node; public DynamicXMLNode(XElement node) { this.node = node; } public DynamicXMLNode() { } public DynamicXMLNode(String name) { node = new XElement(name); } public override bool TrySetMember( SetMemberBinder binder, object value) { XElement setNode = node.Element(binder.Name); if (setNode != null) setNode.SetValue(value); else { if (value.GetType() == typeof(DynamicXMLNode)) node.Add(new XElement(binder.Name)); else node.Add(new XElement(binder.Name, value)); } return true; } public override bool TryGetMember( GetMemberBinder binder, out object result) { XElement getNode = node.Element(binder.Name); if (getNode != null) { result = new DynamicXMLNode(getNode); return true; } else { result = null; return false; } } }
Na prawdę świetny trick! Stworzyliśmy prawdziwy wrapper dla XElement. Teraz zamiast pisać:
XElement contactXML = new XElement("Contact", new XElement("Name", "Patrick Hines"), new XElement("Phone", "206-555-0144"), new XElement("Address", new XElement("Street1", "123 Main St"), new XElement("City", "Mercer Island"), new XElement("State", "WA"), new XElement("Postal", "68042") ) );
Wystarczy:
dynamic contact = new DynamicXMLNode("Contacts"); contact.Name = "Patrick Hines"; contact.Phone = "206-555-0144"; contact.Address = new DynamicXMLNode(); contact.Address.Street = "123 Main St"; contact.Address.City = "Mercer Island"; contact.Address.State = "WA"; contact.Address.Postal = "68402";
DynamicXmlNode to wrapper dla XElement. Kiedykolwiek wywołujemy właściwości typu Address czy Street, wewnątrz wrapper’a ustawiane są odpowiednie pola XmlElement. a zwracany jest dynamiczny DynamicXMLNode. Autorka blogu, pokazuje również jak skonwertować DynamicXMLNodde do string:
public override bool TryConvert( ConvertBinder binder, out object result) { if (binder.Type == typeof(String)) { result = node.Value; return true; } else { result = null; return false; } }
Teraz możliwe jest:
dynamic contact = new DynamicXMLNode("Contacts"); contact.Name = "Patrick Hines"; contact.Phone = "206-555-0144"; contact.Address = new DynamicXMLNode(); contact.Address.Street = "123 Main St"; contact.Address.City = "Mercer Island"; contact.Address.State = "WA"; contact.Address.Postal = "68402"; string text = contact.Phone; // wywoła TryConvert
Osobiście uważam, że warto dodać jeszcze jedną konwersję:
public override bool TryConvert( ConvertBinder binder, out object result) { if (binder.Type == typeof(XElement)) { result = node; return true; } else { result = null; return false; } } private static void Main(string[] args) { dynamic contact = new DynamicXMLNode("Contacts"); contact.Name = "Patrick Hines"; contact.Phone = "206-555-0144"; contact.Address = new DynamicXMLNode(); contact.Address.Street = "123 Main St"; contact.Address.City = "Mercer Island"; contact.Address.State = "WA"; contact.Address.Postal = "68402"; XElement element = contact.Phone; }
W skrócie aby zaimplementować wrapper za pomocą DynamicObject należy:
-
Stworzyć klasę dziedziczącą po DynamicObject.
-
Dodać prywatne pole, które reprezentuje wrappowany element (w powyższych przykładach są to string lub XElement).
-
Przeładować metody, które obsługują operacje zwracania, ustawiania, wywołania metod, konwersji itp.