Zastosowanie dynamic: DynamicObject

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:

  1. Stworzyć klasę dziedziczącą po DynamicObject.
  2. Dodać prywatne pole, które reprezentuje wrappowany element (w powyższych przykładach są to string lub XElement).
  3. Przeładować metody, które obsługują operacje zwracania, ustawiania, wywołania metod, konwersji itp.

Leave a Reply

Your email address will not be published.