Freigeben über


Sunday morning drive with my hair on fire: Types/Community/Synthetic types/Democracy/Cool XML tricks

Exploring types is a pain!

Monad provides Get-Member which makes it pretty nice to explore an OBJECT but if you want to explore that object's type, you have to use the capabilities of the System.RuntimeType class.

Let me make that distinction a little clearer.  Imagine that I've got some xml:
$x=[XML]"<a><b>TEST</b><a>"

I can explore this object using get-member (using the GM alias)

MSH> $x |gm
TypeName: System.Xml.XmlDocument

Name MemberType Definition
---- ---------- ----------
ToString CodeMethod static System.St...
add_NodeChanged Method System.Void add_...
add_NodeChanging Method System.Void add_...
...

But now imagine that I want to exlore System.XML.XMLDocument ([XML]) itself?  Here is what you get:

MSH> [xml]

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False XmlDocument System....

MSH> [xml] |fl *

Module : System.Xml.dll
Assembly : System.Xml, Version=2.0.0.0, Culture=neu
tral, PublicKeyToken=b77a5c561934e089
TypeHandle : System.RuntimeTypeHandle
DeclaringMethod :
BaseType : System.Xml.XmlNode
UnderlyingSystemType : System.Xml.XmlDocument
FullName : System.Xml.XmlDocument
AssemblyQualifiedName : System.Xml.XmlDocument, System.Xml, Vers
ion=2.0.0.0, Culture=neutral, PublicKeyT
oken=b77a5c561934e089
Namespace : System.Xml
GUID : 3d63bf4b-edbe-36cb-b7b2-df87f38db07d
GenericParameterAttributes :
IsGenericTypeDefinition : False
IsGenericParameter : False
GenericParameterPosition :
IsGenericType : False
ContainsGenericParameters : False
StructLayoutAttribute : System.Runtime.InteropServices.StructLay
outAttribute
Name : XmlDocument
MemberType : TypeInfo
DeclaringType :
ReflectedType :
MetadataToken : 33554636
TypeInitializer : Void .cctor()
IsNested : False
Attributes : AutoLayout, AnsiClass, Class, Public, Be
foreFieldInit
IsVisible : True
IsNotPublic : False
IsPublic : True
IsNestedPublic : False
IsNestedPrivate : False
IsNestedFamily : False
IsNestedAssembly : False
IsNestedFamANDAssem : False
IsNestedFamORAssem : False
IsAutoLayout : True
IsLayoutSequential : False
IsExplicitLayout : False
IsClass : True
IsInterface : False
IsValueType : False
IsAbstract : False
IsSealed : False
IsEnum : False
IsSpecialName : False
IsImport : False
IsSerializable : False
IsAnsiClass : True
IsUnicodeClass : False
IsAutoClass : False
IsArray : False
IsByRef : False
IsPointer : False
IsPrimitive : False
IsCOMObject : False
HasElementType : False
IsContextful : False
IsMarshalByRef : False

So that has LOTS of great information but the things that I usually want are the types Constructors, Properties, Methods, Interfaces etc.  That should be easy right?  The type has the method GetConstructors().  Lets see what that gives you.

MSH> [xml].GetConstructors()

Name : .ctor
MemberType : Constructor
DeclaringType : System.Xml.XmlDocument
ReflectedType : System.Xml.XmlDocument
MetadataToken : 100666404
Module : System.Xml.dll
MethodHandle : System.RuntimeMethodHandle
Attributes : PrivateScope, Public, HideBySig, SpecialN
ame, RTSpecialName
CallingConvention : Standard, HasThis
IsGenericMethodDefinition : False
ContainsGenericParameters : False
IsGenericMethod : False
IsPublic : True
IsPrivate : False
IsFamily : False
IsAssembly : False
IsFamilyAndAssembly : False
IsFamilyOrAssembly : False
IsStatic : False
IsFinal : False
IsVirtual : False
IsHideBySig : True
IsAbstract : False
IsSpecialName : True
IsConstructor : True

Name : .ctor
MemberType : Constructor
DeclaringType : System.Xml.XmlDocument
ReflectedType : System.Xml.XmlDocument
MetadataToken : 100666405
Module : System.Xml.dll
MethodHandle : System.RuntimeMethodHandle
Attributes : PrivateScope, Public, HideBySig, SpecialN
ame, RTSpecialName
CallingConvention : Standard, HasThis
IsGenericMethodDefinition : False
ContainsGenericParameters : False
IsGenericMethod : False
IsPublic : True
IsPrivate : False
IsFamily : False
IsAssembly : False
IsFamilyAndAssembly : False
IsFamilyOrAssembly : False
IsStatic : False
IsFinal : False
IsVirtual : False
IsHideBySig : True
IsAbstract : False
IsSpecialName : True
IsConstructor : True

OK - well that is cool and there may be times when I'd like those details (that hasn't happened up to this point in my life but I'm still reasonably young so I'm not ruling it out).  What I want to know is what is the signature of the constructor?  Well if you are smart enough to have an office near Bruce Payette, he may hear you ranting and come in to calm you down and inform you that the signatures area available when you do a ToString() so the way to get that is:

MSH> [xml].GetConstructors() |foreach {"$_"}
Void .ctor()
Void .ctor(System.Xml.XmlNameTable)

Fine.  Now let's shift the discussion and talk about the power of community and the democratization of types. 

Look, its easy to sit back and throw rocks saying - "that is a complex experience, those idiots should have forseen that and made it simple for us".  That rock is completely fair.  Now, if you go the next step and actually file a bug report, you might get this fixed in a couple of years. 

We designed MONAD with this in mind.  My customers don't have a couple of years to wait for the system to be fine tuned for their specific needs - my customers tend to run around with their hair on fire.  They need solutions ASAP.  One of the quickest ways to get a solution is to ask another smart person.  In my case, I was lucky enough to have Bruce just down the hall but with the internet, Bruce is virtually just down the hall from everyone.  (As a side note - I've just started to review a WONDERFUL book that Bruce is writing on Monad so in the future, you'll all be $30 away from having Bruce be in your very own home.)

"Yea - the internet is good and communities help get answers fast" - so what?.  Well that's a pretty big deal but now let's turn our attention to the issue of what form community answers take and the differences in power these different forms take.  For instance, if someone gives you some expository text describing the solution, that is useful but not nearly as useful as when someone gives you a  piece of script that provides a solution. 

Now imagine that this goes on year after year, developer by developer - Where is all that code?  How do people discover it?  How do you maintain it?  If you've pasted the code into your script, then there is a good chance that you've pasted it lots of time in lots of scripts and you've got a maintence mess on your hands.  Also - how does someone (say a new admin) find this?  If you factored it out into it's own script then you still have a discoverability problem even though you've addressed the maintenance problem.

Let's step back and ask the question - why are we doing any of this anyway?  The answer is that the developers that created System.RunTimeType did not implement that class in a way that meets our needs and they can't react quickly enough for us to wait.  But in the end, the best of all solutions is for this function to be implemented as part of the type.  That way when I ask the type what it can do, it will tell me directly.  I don't have to go looking around in lots of scripts looking for whether there are additional functions layered on top. 

Monad addresses this problem with a synthetic type system which allows the democratization of types.  Sure the developers of System.RunTimeType have an important role to play but they are just one voice in this this choir.  Lots of people should be able to extend the type with their own functions. Below is a contents of a file MyTypes.mshxml (attached as well).  This file defines a number of new properties (Constructors, Methods, Properties, Interfaces, Events) for this type.  Type types are implemented using Monad so they are called ScriptProperites and in each case, the call an underlying api (using the $THIS variable which is set to the object being extended), sorting the elements by name and piping the results into a foreach loop which casts the data to a string for easy reading.

<Types>
<Type>
<Name>System.RuntimeType</Name>
<Members>
<ScriptProperty>
<Name>Constructors</Name>
<GetScriptBlock>$this.GetConstructors() | foreach {"$_"}</GetScriptBlock>
</ScriptProperty>
<ScriptProperty>
<Name>Methods</Name>
<GetScriptBlock>$this.GetMethods() |sort name | foreach {"$_"}</GetScriptBlock>
</ScriptProperty>
<ScriptProperty>
<Name>Properties</Name>
<GetScriptBlock>$this.GetProperties() |sort name | foreach {"$_"}</GetScriptBlock>
</ScriptProperty>
<ScriptProperty>
<Name>Interfaces</Name>
<GetScriptBlock>$this.GetInterfaces() |sort name | foreach {"$_"}</GetScriptBlock>
</ScriptProperty>
<ScriptProperty>
<Name>Events</Name>
<GetScriptBlock>$this.GetEvents() |sort name | foreach {"$_"}</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
</Types>

 

The command Update-TypeData MyTypes.mshxml   imports this information and updates the typedata for System.RuntimeType (I have a number of such statements in my profile file).  Now when I inspect types, this information is also available:

MSH> [xml] |fl *

Constructors : {Void .ctor(), Void .ctor(System.Xml.Xml
NameTable)}
Methods : {Void add_NodeChanged(System.Xml.XmlNode
ChangedEventHandler), Void add_NodeChang
ing(System.Xml.XmlNodeChangedEventHandle
...
Properties : {System.Xml.XmlAttributeCollection Attri
butes, System.String BaseURI, System.Xml
...
Interfaces : {System.ICloneable, System.Collections.I
Enumerable, System.Xml.XPath.IXPathNavig
able}
Events : {System.Xml.XmlNodeChangedEventHandler N
odeChanged, System.Xml.XmlNodeChangedEve
...
Module : System.Xml.dll
Assembly : System.Xml, Version=2.0.0.0, Culture=neu
tral, PublicKeyToken=b77a5c561934e089
...
MSH> [xml].constructors
Void .ctor()
Void .ctor(System.Xml.XmlNameTable)

MSH> [xml].interfaces
System.ICloneable
System.Collections.IEnumerable
System.Xml.XPath.IXPathNavigable

MSH> [xml].events
System.Xml.XmlNodeChangedEventHandler NodeChanged
System.Xml.XmlNodeChangedEventHandler NodeChanging
System.Xml.XmlNodeChangedEventHandler NodeInserted
System.Xml.XmlNodeChangedEventHandler NodeInserting
System.Xml.XmlNodeChangedEventHandler NodeRemoved
System.Xml.XmlNodeChangedEventHandler NodeRemoving

See how easy and powerful that is?  Again, we want to build a system which is self-discoverable.  That is why are are hard-core about naming (consistency on naming makes it easy for you to guess what to do and be right).  It is also why we provide interactive exploration tools like Get-Member and the interactive shell.  By using a synthetic type system, we allow new extensions to be added in a way that makes it easy for you to discover and utilize them. 

Cool but wait a minute.  Did you notice that the XML datatype supports the NodeChanged Event.  What's up with that?  Let's check it out.  Rember that I defined $x to be an xml fragment up above, let's use to explore using the Start-NewScope function I provided yesterday:

MSH> $x.Add_NodeChanged({Start-NewScope "NODE CHANGED> "})
MSH> $x.a.b = "New Value"
Starting New Scope
NODE CHANGED> $this | fl *

a : a

 

NODE CHANGED> $_ | fl *

Action : Change
Node : #text
OldParent : b
NewParent : b
OldValue : TEST
NewValue : New Value

 

NODE CHANGED> exit
MSH> $x=[xml]"<a><b>TEST</b></a>"

MSH> $x.Add_NodeChanged({Write-Host $("XMLChange old {0} new {1}" -f $
_.OldValue, $_.NewValue)})

MSH> $x.a.b="NEW-VALUE"
XMLChange old TEST new NEW-VALUE

Is that cool or what?  I would have not known about that unless I was exploring the type.  3 cheers for the democratization of types!  Now just to be clear, democracies can be messy things (that is why I like the analogy).  With lots of people updating the types, you can get name collisions, differing quality levels, etc.  There is no doubt that there are downsides to this model and that if you can get the original development teams to modify that type to meet your needs - it is a better model.  But for those of us that don't have either the bandwidth to engage the original development teams, the ability to convince the teams that our scenarios are going to be needed by 80+% of the people, or the luxury of waiting a few years for the process to work itself out  - there is now a mechanism for us with our hair on fire to stick our heads in a bucket of water.  Sure, doing this might leave us the smell of burnt hair but I'd rather have that than to have my hair still on fire.  :-)

Enjoy!

Jeffrey Snover
Monad Architect

types.mshxml

Comments