Looking for the PowerShell way to write/add delegates

This topic contains 0 replies, has 1 voice, and was last updated by Profile photo of Forums Archives Forums Archives 5 years, 5 months ago.

  • Author
    Posts
  • #5901

    by willsteele at 2012-08-20 09:06:17

    Right now I am working with a lot of scripts to handle XML. One of the scripts I am working on is a port of this C# code to validate an XML file against an XSD:

    http://support.microsoft.com/kb/307379

    My script is as follows:

    [code2=powershell]$xml = 'C:\Data\Documents\Powershell\Projects\PowerShell and XML\types_xsd.xml'

    $FileStream = New-Object -TypeName System.IO.FileStream($xml, `
    [System.IO.FileMode]::Open, `
    [System.IO.FileAccess]::Read, `
    [System.IO.FileShare]::ReadWrite)
    $XmlTextReader = New-Object -TypeName System.Xml.XmlTextReader($FileStream)
    $XmlValidatingReader = New-Object -TypeName System.Xml.XmlValidatingReader($XmlTextReader)
    $XmlValidatingReader.ValidationType = [System.Xml.ValidationType]::Schema
    $XmlValidatingReader.add_ValidationEventHandler($onValidationError)
    While($XmlValidatingReader.Read())
    {
    $onValidationError.Invoke($_)
    }
    $XmlValidatingReader.Close()
    $XmlTextReader.Close()[/code2]

    The problem is I don't quite know how to write/handle delegates, so, the last 10 or so lines (from .add_ValidationEventHandler on) are guesses...and incorrect. The kb article assumes you're working with C#, but, I haven't found any clear way to add delegates in PowerShell scripts. I looked at some of Oisin's posts, but, couldn't quite get 100% of the way there. Ideally, I am trying to report errors (call the delegate whenever an exception is encountered) but am lost on that approach in scripts.

    Below are the XML and XSD definitions I used:

    XML
    [code2=xml]< ?xml version="1.0"?>
    xsi:schemaLocation="note.xsd">
    Tove
    Jani
    Reminder
    Don't forget me this weekend!
    [/code2]

    XSD
    [code2=xml]< ?xml version="1.0" encoding="utf-8"?>








    [/code2]

    Any help understanding delegates would be appreciated. I get the idea, but, the practice is one I never fully got me head around.

    by poshoholic at 2012-08-20 10:44:27

    You should take a look at some of the PowerGUI Script Editor Add-ons I wrote years back. They use delegates/eventing to do a lot of their work. Take the Script Editor Essentials Add-on for example. It defines a script block event handler called $onTabChanged and then associates that with the CurrentDocumentWindowChanged event in the PowerGUI Script Editor. There's a lot of script in that module psm1 file, so just search for $onTabChanged and see how the event handler is implemented as well as how it is associated with the object so that it is invoked when the event is raised.

    If you need more help, let me know.

    by willsteele at 2012-08-20 11:33:45

    Ok, that brings me to here,

    [code2=powershell][System.Xml.Schema.ValidationEventHandler] $onValidationError = {
    param(
    $sender,
    $eventArgs
    )

    $isValid = $false;
    }

    $xml = 'C:\Data\Documents\Powershell\Projects\PowerShell and XML\note.xml'
    $isValid = $true;

    $FileStream = New-Object -TypeName System.IO.FileStream($xml, `
    [System.IO.FileMode]::Open, `
    [System.IO.FileAccess]::Read, `
    [System.IO.FileShare]::ReadWrite)
    $XmlTextReader = New-Object -TypeName System.Xml.XmlTextReader($FileStream)
    $XmlValidatingReader = New-Object -TypeName System.Xml.XmlValidatingReader($XmlTextReader)
    $XmlValidatingReader.ValidationType = [System.Xml.ValidationType]::Schema
    $XmlValidatingReader.add_ValidationEventHandler($onValidationError)
    While($XmlValidatingReader.Read()){}

    $XmlValidatingReader.Close()
    $XmlTextReader.Close()

    # Check whether the document is valid or invalid.
    if ($isValid)
    {
    Write-Output "Document is valid";
    }
    else
    {
    Write-Output "Document is invalid";
    }[/code2]

    When I make an intentional error (remove a closing tag) it throws this error, but, on the PowerShell level:

    [code2=powershell]Read : Exception calling "Read" with "0" argument(s): "The 'body' start tag on line 7 position 4 does not match the end tag of 'note'. Line 8, position 3."

    At C:\Users\wsteele\AppData\Local\Temp\84c00fcd-00cd-479c-b07b-c4afea582964.ps1:54 char:32
    + While($XmlValidatingReader.Read < <<< ()){}
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException[/code2]

    My hope was to simple read out the exception message with the scriptblock.

    by poshoholic at 2012-08-20 11:57:46

    So, if I understand correctly, you're forcing a syntax error by removing a closing tag in the xml, but your using a validation event handler that checks the schema (which isn't the same as the syntax, right)? I haven't dug into Xml at this level before, but my first question is why not let the exceptions get thrown and use try/catch to handle them elegantly and return appropriate error messages? Not that you won't accomplish your goal using delegates and eventing, but I'm just trying to understand the bigger picture.

    Also, doing a quick review of the example for the ValidationEventHandler delegate, I wonder if you might want to include some of the flags if you're going to go the delegate route.

    by willsteele at 2012-08-20 14:20:55

    Yeah, you get points for stating the obvious and being right! I got lost in what I was doing with this one. It is schema validation and messing up the XML file won't affect anything. Let me work on it some more and see if I can find a way to determine why the delegate isn't output text when it is called.

    by willsteele at 2012-08-20 15:27:26

    Ok, I reworked it a little. I am just using the XML/DTD from that kb for simplicity. The DTD is:
    < !ELEMENT Product (ProductName)>
    < !ATTLIST Product ProductID CDATA #REQUIRED>
    < !ELEMENT ProductName (#PCDATA)>

    And the XML is:
    < ?xml version="1.0"?>
    < !DOCTYPE Product SYSTEM "Product.dtd"> Rugby jersey

    To make it error I removed the ProductID attribute from the product in the XML. When I run the following code, it reports, correctly, the document is invalid, based on the bool $isValue. But, the Write-Output in the $onValidationError doesn't seem to get called. I have stepped through and it seems to see it, but, never actually executes the command Write-Output. Am I missing something in the way delegates work?
    [System.Xml.Schema.ValidationEventHandler] $onValidationError = {
    param(
    $sender,
    $eventArgs
    )

    $isValid = $false;
    Write-Output "Error: $($eventArgs)";
    }

    $xml = 'C:\Data\Documents\Powershell\Projects\PowerShell and XML\product.xml'
    $isValid = $true;

    $XmlTextReader = New-Object -TypeName System.Xml.XmlTextReader($Xml)
    $XmlValidatingReader = New-Object -TypeName System.Xml.XmlValidatingReader($XmlTextReader)
    $XmlValidatingReader.ValidationType = [System.Xml.ValidationType]::DTD
    $XmlValidatingReader.add_ValidationEventHandler($onValidationError)
    while($XmlValidatingReader.Read()) {

    }
    $XmlValidatingReader.Close()
    $XmlTextReader.Close()

    # Check whether the document is valid or invalid.
    if ($isValid)
    {
    Write-Output "Document is valid";
    }
    else
    {
    Write-Output "Document is invalid";
    }

    I compiled it in C# and it calls the delegate to output this to the console:
    Validation event
    The required attribute 'ProductID' is missing.
    Document is invalid

    by poshoholic at 2012-08-20 15:48:57

    Delegates work differently than regular scripts. I believe any output that you try to return from them will be suppressed. You should be able to set module-scoped (script:...) variables from them though, and IIRC you should be able to use Write-Host to write messages back to the host (which is probably what you really want to do here instead of Write-Output). Or better yet, Write-Debug or Write-Verbose. But still, the point is the same. You can't simply return objects from a delegate and expect them to appear because you don't have control over how that works. See http://social.msdn.microsoft.com/Forums ... 9d90c20737 for more details about that.

    by willsteele at 2012-08-20 17:00:08

    Perfect. I went with Write-Host and got what I needed. I figured there had to be something mechanical going on that was forcing the output to not be returned. When I debugged I could see the delegate being called, and, the bool was being switched from $true to $false, so, I knew it was firing. At any rate, I got this out of the final deal.
    function Validate-Schema
    {
    < #
    .EXAMPLE 1
    Validate-Schema -XmlFilePath 'C:\PowerShell and XML\product.xml' -SchemaType 'DTD'

    This example illustrates how to validate a DTD schema referenced
    directly from within the .xml file itself. This example includes an
    intentional error and outputs the following:

    Error: The required attribute 'ProductID' is missing.
    Document is invalid

    .PARAMETER XmlFilePath
    A mandatory parameter indicating the path to the .xml file.

    .PARAMETER SchemaType
    A mandatory parameter indicating whether the validation type is DTD
    or XSD. Default is XSD.

    .OUTPUTS
    Boolean.

    If you add the -Verbose switch the function will add comments indicating
    more specifically the item throwing the error during validation.

    .NOTES
    Author: Will Steele (will.steele@live.com)
    Date last modified: 2012/08/20
    By default this script will validate against XSD and will only return
    a boolean value. If you want more verbose output add the -Verbose
    switch to the function when it's called.

    #>

    [CmdletBinding()]
    param(
    [Parameter(
    Mandatory = $true,
    Position = 0
    )]
    [ValidateNotNullOrEmpty()]
    $XmlFilePath,

    [Parameter(
    Mandatory = $true,
    Position = 1
    )]
    [ValidateNotNullOrEmpty()]
    [ValidateSet('DTD','XSD')]
    $SchemaType = 'XSD'
    )

    # Create delegate for handling/reporting errors
    [System.Xml.Schema.ValidationEventHandler] $onValidationError = {
    param(
    $sender,
    $eventArgs
    )

    $isValid = $false;
    Write-Verbose "Error: $($eventArgs.Message)";
    }

    # Set while loop flag
    $isValid = $true;

    # Instantiate XML text reader
    $XmlTextReader = New-Object -TypeName System.Xml.XmlTextReader($XmlFilePath)

    # Instantiate ValidatingReader and set ValidationType
    $XmlValidatingReader = New-Object -TypeName System.Xml.XmlValidatingReader($XmlTextReader)
    switch($SchemaType)
    {
    'DTD'
    {
    $XmlValidatingReader.ValidationType = [System.Xml.ValidationType]::DTD;
    }
    'XSD'
    {
    $XmlValidatingReader.ValidationType = [System.Xml.ValidationType]::Schema;
    }
    }

    # Add handler to Validating Reader
    $XmlValidatingReader.add_ValidationEventHandler($onValidationError)

    # Validate file
    while($XmlValidatingReader.Read()) {}

    # Close handles
    $XmlValidatingReader.Close()
    $XmlTextReader.Close()

    # Output whether the document is valid or invalid.
    if ($isValid)
    {
    Write-Verbose "Document is valid";
    $isValid
    }
    else
    {
    Write-Verbose "Document is invalid";
    $isValid
    }
    }

    Validate-Schema -XmlFilePath 'C:\Data\Documents\Powershell\Projects\PowerShell and XML\product.xml' -SchemaType 'DTD'
    When I run it, calling out to my "incorrect" DTD I get the following:
    Error: The required attribute 'ProductID' is missing.
    Document is invalid

    by MattG at 2012-08-20 18:01:26

    Hey Will,

    I'm glad you figured out your issue! I wanted to elaborate on delegates though in the hopes that someone can be spared from the pain I went through to implement the 'delegate' keyword in PowerShell. I wrote the Get-DelegateType function to implement an equivalent to the C# 'delegate' keyword. I've been using this function in some of my PowerSploit scripts in order to avoid using P/Invoke to call Win32 API functions.

    [script=powershell]function Get-DelegateType
    {
    Param
    (
    [OutputType([Type])]

    [Parameter( Position = 0)]
    [Type[]]
    $Parameters = (New-Object Type[](0)),

    [Parameter( Position = 1 )]
    [Type]
    $ReturnType = [Void]
    )

    $Domain = [AppDomain]::CurrentDomain
    $DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
    $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
    $TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
    $ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters)
    $ConstructorBuilder.SetImplementationFlags('Runtime, Managed')
    $MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters)
    $MethodBuilder.SetImplementationFlags('Runtime, Managed')

    Write-Output $TypeBuilder.CreateType()
    }[/script]

    The function will output a Type object of type Delegate. You provide it with parameters and a return type and it will define a delegate method signature and essentially creates a function pointer for you. As an example, I'll create a delegate for the [Convert]]) method:

    [script=powershell]$ToInt64DelegateType = Get-DelegateType -Parameters @([Int32]) -ReturnType ([Int64])
    $ToInt32 = [Delegate]::CreateDelegate($ToInt64DelegateType, ([Convert].GetMethod('ToInt64', [Type[]] @([Int32]))))
    $ToInt32.Invoke(4)
    $ToInt32.Invoke(4).GetType()[/script]
    Hopefully, my example is a sufficient PowerShell analog to the C# techniques described here: Delegates (C#)

    I hope this helps!

    by willsteele at 2012-08-20 18:19:34

    Thanks Matt. After I get my eyes uncrossed I'll read it. : ) Nice looking function though, seriously. Let me digest it a bit.

You must be logged in to reply to this topic.