The Labs \ Source Viewer \ SSCLI \ System.ComponentModel \ ReflectEventDescriptor

  1. //------------------------------------------------------------------------------
  2. // <copyright file="ReflectEventDescriptor.cs" company="Microsoft">
  3. //
  4. // Copyright (c) 2006 Microsoft Corporation. All rights reserved.
  5. //
  6. // The use and distribution terms for this software are contained in the file
  7. // named license.txt, which can be found in the root of this distribution.
  8. // By using this software in any fashion, you are agreeing to be bound by the
  9. // terms of this license.
  10. //
  11. // You must not remove this notice, or any other, from this software.
  12. //
  13. // </copyright>
  14. //------------------------------------------------------------------------------
  15. /*
  16. */
  17. namespace System.ComponentModel
  18. {
  19.     using Microsoft.Win32;
  20.     using System;
  21.     using System.Collections;
  22.     using System.ComponentModel.Design;
  23.     using System.Diagnostics;
  24.     using System.Reflection;
  25.     using System.Security.Permissions;
  26.    
  27.     /// <internalonly/>
  28.     /// <devdoc>
  29.     /// <para>
  30.     /// ReflectEventDescriptor defines an event. Events are the main way that a user can get
  31.     /// run-time notifications from a component.
  32.     /// The ReflectEventDescriptor class takes a component class that the event lives on,
  33.     /// the event name, the type of the event handling delegate, and various
  34.     /// attributes for the event.
  35.     /// Every event has a structure through which it passes it's information. The base
  36.     /// structure, Event, is empty, and there is a default instance, Event.EMPTY, which
  37.     /// is usually passed. When addOnXXX is invoked, it needs a pointer to a method
  38.     /// that takes a source object (the object that fired the event) and a structure
  39.     /// particular to that event. It also needs a pointer to the instance of the
  40.     /// object the method should be invoked on. These two things are what composes a
  41.     /// delegate. An event handler is
  42.     /// a delegate, and the compiler recognizes a special delegate syntax that makes
  43.     /// using delegates easy.
  44.     /// For example, to listen to the click event on a button in class Foo, the
  45.     /// following code will suffice:
  46.     /// </para>
  47.     /// <code>
  48.     /// class Foo {
  49.     /// Button button1 = new Button();
  50.     /// void button1_click(Object sender, Event e) {
  51.     /// // do something on button1 click.
  52.     /// }
  53.     /// public Foo() {
  54.     /// button1.addOnClick(this.button1_click);
  55.     /// }
  56.     /// }
  57.     /// </code>
  58.     /// For an event named XXX, a YYYEvent structure, and a YYYEventHandler delegate,
  59.     /// a component writer is required to implement two methods of the following
  60.     /// form:
  61.     /// <code>
  62.     /// public void addOnXXX(YYYEventHandler handler);
  63.     /// public void removeOnXXX(YYYEventHandler handler);
  64.     /// </code>
  65.     /// YYYEventHandler should be an event handler declared as
  66.     /// <code>
  67.     /// public multicast delegate void YYYEventHandler(Object sender, YYYEvent e);
  68.     /// </code>
  69.     /// Note that this event was declared as a multicast delegate. This allows multiple
  70.     /// listeners on an event. This is not a requirement.
  71.     /// Various attributes can be passed to the ReflectEventDescriptor, as are described in
  72.     /// Attribute.
  73.     /// ReflectEventDescriptors can be obtained by a user programmatically through the
  74.     /// ComponentManager.
  75.     /// </devdoc>
  76.     [HostProtection(SharedState = true)]
  77.     internal sealed class ReflectEventDescriptor : EventDescriptor
  78.     {
  79.        
  80.         private static readonly Type[] argsNone = new Type[0];
  81.         private static readonly object noDefault = new object();
  82.        
  83.         private Type type;
  84.         // the delegate type for the event
  85.         private readonly Type componentClass;
  86.         // the class of the component this info is for
  87.         private MethodInfo addMethod;
  88.         // the method to use when adding an event
  89.         private MethodInfo removeMethod;
  90.         // the method to use when removing an event
  91.         private EventInfo realEvent;
  92.         // actual event info... may be null
  93.         private bool filledMethods = false;
  94.         // did we already call FillMethods() once?
  95.         /// <devdoc>
  96.         /// This is the main constructor for an ReflectEventDescriptor.
  97.         /// </devdoc>
  98.         public ReflectEventDescriptor(Type componentClass, string name, Type type, Attribute[] attributes) : base(name, attributes)
  99.         {
  100.             if (componentClass == null) {
  101.                 throw new ArgumentException(SR.GetString(SR.InvalidNullArgument, "componentClass"));
  102.             }
  103.             if (type == null || !(typeof(Delegate)).IsAssignableFrom(type)) {
  104.                 throw new ArgumentException(SR.GetString(SR.ErrorInvalidEventType, name));
  105.             }
  106.             Debug.Assert(type.IsSubclassOf(typeof(Delegate)), "Not a valid ReflectEvent: " + componentClass.FullName + "." + name + " " + type.FullName);
  107.             this.componentClass = componentClass;
  108.             this.type = type;
  109.         }
  110.        
  111.         public ReflectEventDescriptor(Type componentClass, EventInfo eventInfo) : base(eventInfo.Name, new Attribute[0])
  112.         {
  113.            
  114.             if (componentClass == null) {
  115.                 throw new ArgumentException(SR.GetString(SR.InvalidNullArgument, "componentClass"));
  116.             }
  117.             this.componentClass = componentClass;
  118.             this.realEvent = eventInfo;
  119.         }
  120.        
  121.         /// <devdoc>
  122.         /// This constructor takes an existing ReflectEventDescriptor and modifies it by merging in the
  123.         /// passed-in attributes.
  124.         /// </devdoc>
  125.         public ReflectEventDescriptor(Type componentType, EventDescriptor oldReflectEventDescriptor, Attribute[] attributes) : base(oldReflectEventDescriptor, attributes)
  126.         {
  127.             this.componentClass = componentType;
  128.             this.type = oldReflectEventDescriptor.EventType;
  129.            
  130.             ReflectEventDescriptor desc = oldReflectEventDescriptor as ReflectEventDescriptor;
  131.             if (desc != null) {
  132.                 this.addMethod = desc.addMethod;
  133.                 this.removeMethod = desc.removeMethod;
  134.                 this.filledMethods = true;
  135.             }
  136.             #if DEBUG
  137.             else if (oldReflectEventDescriptor is DebugReflectEventDescriptor) {
  138.                 this.addMethod = ((DebugReflectEventDescriptor)oldReflectEventDescriptor).addMethod;
  139.                 this.removeMethod = ((DebugReflectEventDescriptor)oldReflectEventDescriptor).removeMethod;
  140.                 this.filledMethods = true;
  141.             }
  142.             #endif
  143.         }
  144.        
  145.        
  146.        
  147.         /// <devdoc>
  148.         /// Retrieves the type of the component this EventDescriptor is bound to.
  149.         /// </devdoc>
  150.         public override Type ComponentType {
  151.             get { return componentClass; }
  152.         }
  153.        
  154.         /// <devdoc>
  155.         /// Retrieves the type of the delegate for this event.
  156.         /// </devdoc>
  157.         public override Type EventType {
  158.             get {
  159.                 FillMethods();
  160.                 return type;
  161.             }
  162.         }
  163.        
  164.         /// <devdoc>
  165.         /// Indicates whether the delegate type for this event is a multicast
  166.         /// delegate.
  167.         /// </devdoc>
  168.         public override bool IsMulticast {
  169.             get { return (typeof(MulticastDelegate)).IsAssignableFrom(EventType); }
  170.         }
  171.        
  172.         /// <devdoc>
  173.         /// This adds the delegate value as a listener to when this event is fired
  174.         /// by the component, invoking the addOnXXX method.
  175.         /// </devdoc>
  176.         public override void AddEventHandler(object component, Delegate value)
  177.         {
  178.             FillMethods();
  179.            
  180.             if (component != null) {
  181.                 ISite site = GetSite(component);
  182.                 IComponentChangeService changeService = null;
  183.                
  184.                 // Announce that we are about to change this component
  185.                 //
  186.                 if (site != null) {
  187.                     changeService = (IComponentChangeService)site.GetService(typeof(IComponentChangeService));
  188.                     Debug.Assert(!CompModSwitches.CommonDesignerServices.Enabled || changeService != null, "IComponentChangeService not found");
  189.                 }
  190.                
  191.                 if (changeService != null) {
  192.                     try {
  193.                         changeService.OnComponentChanging(component, this);
  194.                     }
  195.                     catch (CheckoutException coEx) {
  196.                         if (coEx == CheckoutException.Canceled) {
  197.                             return;
  198.                         }
  199.                         throw coEx;
  200.                     }
  201.                 }
  202.                
  203.                 bool shadowed = false;
  204.                
  205.                 if (site != null && site.DesignMode) {
  206.                     // Events are final, so just check the class
  207.                     if (EventType != value.GetType()) {
  208.                         throw new ArgumentException(SR.GetString(SR.ErrorInvalidEventHandler, Name));
  209.                     }
  210.                     IDictionaryService dict = (IDictionaryService)site.GetService(typeof(IDictionaryService));
  211.                     Debug.Assert(!CompModSwitches.CommonDesignerServices.Enabled || dict != null, "IDictionaryService not found");
  212.                     if (dict != null) {
  213.                         Delegate eventdesc = (Delegate)dict.GetValue(this);
  214.                         eventdesc = Delegate.Combine(eventdesc, value);
  215.                         dict.SetValue(this, eventdesc);
  216.                         shadowed = true;
  217.                     }
  218.                 }
  219.                
  220.                 if (!shadowed) {
  221.                     addMethod.Invoke(component, new object[] {value});
  222.                 }
  223.                
  224.                 // Now notify the change service that the change was successful.
  225.                 //
  226.                 if (changeService != null) {
  227.                     changeService.OnComponentChanged(component, this, null, value);
  228.                 }
  229.             }
  230.         }
  231.        
  232.         // <doc>
  233.         // <desc>
  234.         // Adds in custom attributes found on either the AddOn or RemoveOn method...
  235.         // </desc>
  236.         // </doc>
  237.         //
  238.         protected override void FillAttributes(IList attributes)
  239.         {
  240.            
  241.             //
  242.             // The order that we fill in attributes is critical. The list of attributes will be
  243.             // filtered so that matching attributes at the end of the list replace earlier matches
  244.             // (last one in wins). Therefore, the two categories of attributes we add must be
  245.             // added as follows:
  246.             //
  247.             // 1. Attributes of the event, from base class to most derived. This way
  248.             // derived class attributes replace base class attributes.
  249.             //
  250.             // 2. Attributes from our base MemberDescriptor. While this seems opposite of what
  251.             // we want, MemberDescriptor only has attributes if someone passed in a new
  252.             // set in the constructor. Therefore, these attributes always
  253.             // supercede existing values.
  254.             //
  255.            
  256.             FillMethods();
  257.             Debug.Assert(componentClass != null, "Must have a component class for FilterAttributes");
  258.             if (realEvent != null) {
  259.                 FillEventInfoAttribute(realEvent, attributes);
  260.             }
  261.             else {
  262.                 Debug.Assert(removeMethod != null, "Null remove method for " + Name);
  263.                 FillSingleMethodAttribute(removeMethod, attributes);
  264.                
  265.                 Debug.Assert(addMethod != null, "Null remove method for " + Name);
  266.                 FillSingleMethodAttribute(addMethod, attributes);
  267.             }
  268.            
  269.             // Include the base attributes. These override all attributes on the actual
  270.             // property, so we want to add them last.
  271.             //
  272.             base.FillAttributes(attributes);
  273.         }
  274.        
  275.         private void FillEventInfoAttribute(EventInfo realEventInfo, IList attributes)
  276.         {
  277.             string eventName = realEventInfo.Name;
  278.             BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;
  279.             Type currentReflectType = realEventInfo.ReflectedType;
  280.             Debug.Assert(currentReflectType != null, "currentReflectType cannot be null");
  281.             int depth = 0;
  282.            
  283.             // First, calculate the depth of the object hierarchy. We do this so we can do a single
  284.             // object create for an array of attributes.
  285.             //
  286.             while (currentReflectType != typeof(object)) {
  287.                 depth++;
  288.                 currentReflectType = currentReflectType.BaseType;
  289.             }
  290.            
  291.             if (depth > 0) {
  292.                 // Now build up an array in reverse order
  293.                 //
  294.                 currentReflectType = realEventInfo.ReflectedType;
  295.                 Attribute[][] attributeStack = new Attribute[depth][];
  296.                
  297.                 while (currentReflectType != typeof(object)) {
  298.                    
  299.                     // Fill in our member info so we can get at the custom attributes.
  300.                     //
  301.                     MemberInfo memberInfo = currentReflectType.GetEvent(eventName, bindingFlags);
  302.                    
  303.                     // Get custom attributes for the member info.
  304.                     //
  305.                     if (memberInfo != null) {
  306.                         attributeStack[--depth] = ReflectTypeDescriptionProvider.ReflectGetAttributes(memberInfo);
  307.                     }
  308.                    
  309.                     // Ready for the next loop iteration.
  310.                     //
  311.                     currentReflectType = currentReflectType.BaseType;
  312.                 }
  313.                
  314.                 // Now trawl the attribute stack so that we add attributes
  315.                 // from base class to most derived.
  316.                 //
  317.                 foreach (Attribute[] attributeArray in attributeStack) {
  318.                     if (attributeArray != null) {
  319.                         foreach (Attribute attr in attributeArray) {
  320.                             attributes.Add(attr);
  321.                         }
  322.                     }
  323.                 }
  324.             }
  325.         }
  326.        
  327.         /// <devdoc>
  328.         /// This fills the get and set method fields of the event info. It is shared
  329.         /// by the various constructors.
  330.         /// </devdoc>
  331.         private void FillMethods()
  332.         {
  333.             if (filledMethods)
  334.                 return;
  335.            
  336.             if (realEvent != null) {
  337.                 addMethod = realEvent.GetAddMethod();
  338.                 removeMethod = realEvent.GetRemoveMethod();
  339.                
  340.                 EventInfo defined = null;
  341.                
  342.                 if (addMethod == null || removeMethod == null) {
  343.                     Type start = componentClass.BaseType;
  344.                     while (start != null && start != typeof(object)) {
  345.                         BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
  346.                         EventInfo test = start.GetEvent(realEvent.Name, bindingFlags);
  347.                         if (test.GetAddMethod() != null) {
  348.                             defined = test;
  349.                             break;
  350.                         }
  351.                     }
  352.                 }
  353.                
  354.                 if (defined != null) {
  355.                     addMethod = defined.GetAddMethod();
  356.                     removeMethod = defined.GetRemoveMethod();
  357.                     type = defined.EventHandlerType;
  358.                 }
  359.                 else {
  360.                     type = realEvent.EventHandlerType;
  361.                 }
  362.             }
  363.             else {
  364.                
  365.                 // first, try to get the eventInfo...
  366.                 //
  367.                 realEvent = this.componentClass.GetEvent(Name);
  368.                 if (realEvent != null) {
  369.                     // if we got one, just recurse and return.
  370.                     //
  371.                     FillMethods();
  372.                     return;
  373.                 }
  374.                
  375.                 Type[] argsType = new Type[] {type};
  376.                 addMethod = FindMethod(componentClass, "AddOn" + Name, argsType, typeof(void));
  377.                 removeMethod = FindMethod(componentClass, "RemoveOn" + Name, argsType, typeof(void));
  378.                 if (addMethod == null || removeMethod == null) {
  379.                     Debug.Fail("Missing event accessors for " + componentClass.FullName + "." + Name);
  380.                     throw new ArgumentException(SR.GetString(SR.ErrorMissingEventAccessors, Name));
  381.                 }
  382.             }
  383.            
  384.             filledMethods = true;
  385.         }
  386.        
  387.         private void FillSingleMethodAttribute(MethodInfo realMethodInfo, IList attributes)
  388.         {
  389.            
  390.             string methodName = realMethodInfo.Name;
  391.             BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;
  392.             Type currentReflectType = realMethodInfo.ReflectedType;
  393.             Debug.Assert(currentReflectType != null, "currentReflectType cannot be null");
  394.            
  395.             // First, calculate the depth of the object hierarchy. We do this so we can do a single
  396.             // object create for an array of attributes.
  397.             //
  398.             int depth = 0;
  399.             while (currentReflectType != null && currentReflectType != typeof(object)) {
  400.                 depth++;
  401.                 currentReflectType = currentReflectType.BaseType;
  402.             }
  403.            
  404.             if (depth > 0) {
  405.                 // Now build up an array in reverse order
  406.                 //
  407.                 currentReflectType = realMethodInfo.ReflectedType;
  408.                 Attribute[][] attributeStack = new Attribute[depth][];
  409.                
  410.                 while (currentReflectType != null && currentReflectType != typeof(object)) {
  411.                     // Fill in our member info so we can get at the custom attributes.
  412.                     //
  413.                     MemberInfo memberInfo = currentReflectType.GetMethod(methodName, bindingFlags);
  414.                    
  415.                     // Get custom attributes for the member info.
  416.                     //
  417.                     if (memberInfo != null) {
  418.                         attributeStack[--depth] = ReflectTypeDescriptionProvider.ReflectGetAttributes(memberInfo);
  419.                     }
  420.                    
  421.                     // Ready for the next loop iteration.
  422.                     //
  423.                     currentReflectType = currentReflectType.BaseType;
  424.                 }
  425.                
  426.                 // Now trawl the attribute stack so that we add attributes
  427.                 // from base class to most derived.
  428.                 //
  429.                 foreach (Attribute[] attributeArray in attributeStack) {
  430.                     if (attributeArray != null) {
  431.                         foreach (Attribute attr in attributeArray) {
  432.                             attributes.Add(attr);
  433.                         }
  434.                     }
  435.                 }
  436.             }
  437.         }
  438.        
  439.         /// <devdoc>
  440.         /// This will remove the delegate value from the event chain so that
  441.         /// it no longer gets events from this component.
  442.         /// </devdoc>
  443.         public override void RemoveEventHandler(object component, Delegate value)
  444.         {
  445.             FillMethods();
  446.            
  447.             if (component != null) {
  448.                 ISite site = GetSite(component);
  449.                 IComponentChangeService changeService = null;
  450.                
  451.                 // Announce that we are about to change this component
  452.                 //
  453.                 if (site != null) {
  454.                     changeService = (IComponentChangeService)site.GetService(typeof(IComponentChangeService));
  455.                     Debug.Assert(!CompModSwitches.CommonDesignerServices.Enabled || changeService != null, "IComponentChangeService not found");
  456.                 }
  457.                
  458.                 if (changeService != null) {
  459.                     try {
  460.                         changeService.OnComponentChanging(component, this);
  461.                     }
  462.                     catch (CheckoutException coEx) {
  463.                         if (coEx == CheckoutException.Canceled) {
  464.                             return;
  465.                         }
  466.                         throw coEx;
  467.                     }
  468.                 }
  469.                
  470.                 bool shadowed = false;
  471.                
  472.                 if (site != null && site.DesignMode) {
  473.                     IDictionaryService dict = (IDictionaryService)site.GetService(typeof(IDictionaryService));
  474.                     Debug.Assert(!CompModSwitches.CommonDesignerServices.Enabled || dict != null, "IDictionaryService not found");
  475.                     if (dict != null) {
  476.                         Delegate del = (Delegate)dict.GetValue(this);
  477.                         del = Delegate.Remove(del, value);
  478.                         dict.SetValue(this, del);
  479.                         shadowed = true;
  480.                     }
  481.                 }
  482.                
  483.                 if (!shadowed) {
  484.                     removeMethod.Invoke(component, new object[] {value});
  485.                 }
  486.                
  487.                 // Now notify the change service that the change was successful.
  488.                 //
  489.                 if (changeService != null) {
  490.                     changeService.OnComponentChanged(component, this, null, value);
  491.                 }
  492.             }
  493.         }
  494.        
  495.        
  496.         /* The following code has been removed to fix FXCOP violations.
  497.           its left here incase it needs to be resurected. This code is from
  498.           ReflectEventDescriptor class
  499.         /// <devdoc>
  500.         ///    This is a shortcut main constructor for an ReflectEventDescriptor with one attribute.
  501.         /// </devdoc>
  502.         public ReflectEventDescriptor(Type componentClass, string name, Type type, MethodInfo addMethod, MethodInfo removeMethod) : this(componentClass, name, type, (Attribute[]) null) {
  503.             this.addMethod = addMethod;
  504.             this.removeMethod = removeMethod;
  505.             this.filledMethods = true;
  506.         }
  507.         /// <devdoc>
  508.         ///    This is a shortcut main constructor for an ReflectEventDescriptor with one attribute.
  509.         /// </devdoc>
  510.         public ReflectEventDescriptor(Type componentClass, string name, Type type) : this(componentClass, name, type, (Attribute[]) null) {
  511.         }
  512.         /// <devdoc>
  513.         ///    This is a shortcut main constructor for an ReflectEventDescriptor with two attributes.
  514.         /// </devdoc>
  515.         public ReflectEventDescriptor(Type componentClass, string name, Type type,
  516.                                       Attribute a1) : this(componentClass, name, type, new Attribute[] {a1}) {
  517.         }
  518.         /// <devdoc>
  519.         ///    This is a shortcut main constructor for an ReflectEventDescriptor with two attributes.
  520.         /// </devdoc>
  521.         public ReflectEventDescriptor(Type componentClass, string name, Type type,
  522.                                       Attribute a1, Attribute a2) : this(componentClass, name, type, new Attribute[] {a1, a2}) {
  523.         }
  524.         /// <devdoc>
  525.         ///    This is a shortcut main constructor for an ReflectEventDescriptor with three attributes.
  526.         /// </devdoc>
  527.         public ReflectEventDescriptor(Type componentClass, string name, Type type,
  528.                                       Attribute a1, Attribute a2, Attribute a3) : this(componentClass, name, type, new Attribute[] {a1, a2, a3}) {
  529.         }
  530.         /// <devdoc>
  531.         ///    This is a shortcut main constructor for an ReflectEventDescriptor with four attributes.
  532.         /// </devdoc>
  533.         public ReflectEventDescriptor(Type componentClass, string name, Type type,
  534.                                       Attribute a1, Attribute a2,
  535.                                       Attribute a3, Attribute a4) : this(componentClass, name, type, new Attribute[] {a1, a2, a3, a4}) {
  536.         }
  537.         /// <devdoc>
  538.         ///    This constructor takes an existing ReflectEventDescriptor and modifies it by merging in the
  539.         ///    passed-in attributes.
  540.         /// </devdoc>
  541.         public ReflectEventDescriptor(EventDescriptor oldReflectEventDescriptor, Attribute[] attributes)
  542.         : this(oldReflectEventDescriptor.ComponentType, oldReflectEventDescriptor, attributes) {
  543.         }
  544.         /// <devdoc>
  545.         ///    This is a shortcut constructor that takes an existing ReflectEventDescriptor and one attribute to
  546.         ///    merge in.
  547.         /// </devdoc>
  548.         public ReflectEventDescriptor(EventDescriptor oldReflectEventDescriptor, Attribute a1) : this(oldReflectEventDescriptor, new Attribute[] { a1}) {
  549.         }
  550.         /// <devdoc>
  551.         ///    This is a shortcut constructor that takes an existing ReflectEventDescriptor and two attributes to
  552.         ///    merge in.
  553.         /// </devdoc>
  554.         public ReflectEventDescriptor(EventDescriptor oldReflectEventDescriptor, Attribute a1,
  555.                                       Attribute a2) : this(oldReflectEventDescriptor, new Attribute[] { a1,a2}) {
  556.         }
  557.         /// <devdoc>
  558.         ///    This is a shortcut constructor that takes an existing ReflectEventDescriptor and three attributes to
  559.         ///    merge in.
  560.         /// </devdoc>
  561.         public ReflectEventDescriptor(EventDescriptor oldReflectEventDescriptor, Attribute a1,
  562.                                       Attribute a2, Attribute a3) : this(oldReflectEventDescriptor, new Attribute[] { a1,a2,a3}) {
  563.         }
  564.         /// <devdoc>
  565.         ///    This is a shortcut constructor that takes an existing ReflectEventDescriptor and four attributes to
  566.         ///    merge in.
  567.         /// </devdoc>
  568.         public ReflectEventDescriptor(EventDescriptor oldReflectEventDescriptor, Attribute a1,
  569.                                       Attribute a2, Attribute a3, Attribute a4) : this(oldReflectEventDescriptor, new Attribute[] { a1,a2,a3,a4}) {
  570.         }
  571.         */       
  572.        
  573.     }
  574. }

Developer Fusion