пятница, 28 мая 2021 г.

Возвращение поддержки внедренных скриптов в XSLT в .Net Core и .Net 5

 

 

Возвращение поддержки внедренных скриптов в XSLT в .Net Core и .Net 5

В .Net Framework для XSLT поддерживалась возможность включать в XSLT скрипты, написанные на нескольких языках программирования (C#, VB.Net, JScript.Net). Функции, описанные в этих скриптах можно использовать в выражениях XPath, благодаря чему возможности языка (довольно скромные, если использовать только стандарты) возрастают многократно. В следующих версиях XSLT (тех, что выше первой) многое поменялось и необходимость в их расширении возникает уже не так часто, но в .Net более поздние версии XSLT не поддерживаются и реализация такой поддержки в стандартных библиотеках не планируется. Использование внешних инструментов имеет свои ограничения, по крайней мере для бесплатных версий. Поддержка же возможности расширения языка собственным кодом, написанным на упомянутых языках, доводит возможности XSLT до такого уровня, что его хватает для решения практически любых задач.

Добавлять свои функции в преобразование можно двумя способами: функции можно описать в самой программе, выполняющей преобразование; и в скриптах, размещенных непосредственно в документе XSLT. Возможности, которые дают оба способа – одинаковы. По этой причине может показаться, что внедренные скрипты не очень-то и нужны. Но объекты расширения, добавляемые в коде программы должны быть описаны самой программе и предварительно скомпилированы. Таким образом, если возникнет необходимость добавить какую-то новую функцию, придется внести ее в код программы. Ну и соответственно, в этом случае придется заранее позаботиться о том, чтобы расширение обладало достаточно большим набором функций. В то же время преимущество XSLT, на мой взгляд, как раз в том и состоит, что этот язык позволяет вынести логику трансформации документа за пределы программного кода. В этом смысле удаление поддержки скриптов – достаточно серьезная потеря, которую хотелось бы как-то компенсировать.

Немного теории

За преобразование XSLT у нас отвечает класс XslCompiledTransform. Об использовании внедренных скриптов можно почитать здесь Использование скриптов таблиц стилей XSLT<msxsl:script>.  Добавление объектов расширения в коде программы осуществляется с помощью класса XsltArgumentList.

Внедренный скрипт фактически представляет из себя код класса без самой декларации класса и фактически это действует ровно также, как и при использовании объектов расширения, где объект должен содержать некоторый набор методов, которые и будут доступны в выражениях XPath. В тоже самое время XSLT является словарем XML, благодаря чему у нас есть возможность его предварительной обработки, используя при этом инструменты работы с XML. Таким образом, если мы хотим, чтобы выполнялись скрипты, нам для этого нужно в процессе загрузки XSLT извлечь из него все скрипты, сформировать на их основе классы, скомпилировать результат, создать экземпляры классов и в процессе преобразования добавить их XsltArgumentList как объекты расширения. Собственно, этим мы сейчас и будем заниматься.

Процессор

Класс, отвечающий за выполнение преобразования должен быть неким подобием класса XsltCompiledTransform. То есть в нем должны быть методы Load и Transform. Это важно из-за того, что в процессе загрузки документа XSLT будет выполняться некоторая предварительная обработка. В частности, мы будем извлекать скрипты и компилировать их. Динамическая компиляция занимает некоторое время и если обрабатывается только один документ, то можно процессы загрузки и собственно преобразования объединить в один метод. Но мы будем исходить из того, что бывают случаи, когда одно преобразование используется для целого пакета документов и в этом случае секунды, потраченные на компиляцию нужно будет умножить на количество документов в пакете, что конечно же уже будет занимать немало времени. В то же время, если мы будет выполнять компиляцию скриптов при загрузке, то при выполнении преобразования у нас уже все будет скомпилировано.

Поскольку код скрипта не является полноценным кодом, готовым к компиляции, для начала нам потребуется из кода скрипта создать полноценный код класса. Поскольку мы хотим реализовать поддержку максимально возможного количества языков, нам придется позаботиться о том, чтобы код формировался правильно независимо от языка. Удобнее всего для этого создать дерево кода из объектов CodeDom.  И так, добавляем в код процессора метод, который будет на основе скрипта, представленного XElement, создавать код класса.

   Private Function GenerateCode(script As XElement) As String

        Dim provider = cdc.CodeDomProvider.CreateProvider(script.@language)

        Dim unit As New cd.CodeCompileUnit

        Dim ns1 As New cd.CodeNamespace("Ns1")

        unit.Namespaces.Add(ns1)

        Dim class1 As New cd.CodeTypeDeclaration("Class1")

        ns1.Types.Add(class1)

 

        Dim defaultns = $"System,System.Collections,System.Text,System.Text.RegularExpressions,System.Xml,System.Xml.Xsl,System.Xml.XPath".

            Split(","c, StringSplitOptions.RemoveEmptyEntries).

            Concat(script.<msxsl:using>.Select(Function(ns) ns.@namespace)).Distinct().

            Select(Function(ns) New CodeDom.CodeNamespaceImport(ns))

        ns1.Imports.AddRange(defaultns.ToArray)

        If "vb visualbasic".Split(" "c).Contains(script.@language.ToLower) Then

            ns1.Imports.Add(New CodeDom.CodeNamespaceImport("Microsoft.VisualBasic"))

        End If

        Dim codeText = New cd.CodeSnippetTypeMember(script.Value)

        class1.Members.Add(codeText)

        Dim sb As New StringBuilder

        Using writer As New IO.StringWriter(sb)

            provider.GenerateCodeFromCompileUnit(unit, writer, New CodeDom.Compiler.CodeGeneratorOptions())

        End Using

        Return sb.ToString

    End Function

 

На что здесь следует обратить внимание. Класс, созданный этим методом будет всегда иметь имя Ns1.Class1. Это не будет иметь значения в том случае, если сборка имеет только один этот класс. Если же мы захотим собрать по всему документу все скрипты и скомпилировать их в одну сборку, это приведет к конфликту имен и в этом случае логику именования придется изменить.

Второй момент: по умолчанию я добавил несколько пространств имен. Когда внедренные скрипты поддерживались там происходило ровно то же самое. То есть там предусмотрена возможность импортировать любые пространства имен, но даже без этого несколько пространств импортируется без этого. Естественно этого можно либо вообще не делать, либо изменить этот список по своему усмотрению.

Далее нам следует позаботиться о том, чтобы из каждого скрипта документа создавался объект расширения и добавлялся в список таких объектов. Добавление XsltArgumentList можно осуществить так же как это делается в XsltCompiledTransform, то есть добавить несколько перегрузок метода Transform, содержащих параметр этого типа. Но мне показалось, что будет более удобно создавать этот объект автоматически при загрузке XSLT, добавлять в него все что нужно и при выполнении преобразования использовать этот объект. Но на случай, если нужно будет еще и «вручную» добавить что-то в этот список, нужно будет предоставить доступ к этому объекту. Таким образом нам понадобится следующее свойство

    Dim _ArgumentList As XsltArgumentList

 

    Public ReadOnly Property ArgumentList As XsltArgumentList

        Get

            Return _ArgumentList

        End Get

    End Property

 

Теперь о методе, который будет добавлять объекты в список. Сам метод будет принимать объект скрипта (XElement), далее его задача извлечь код, сформировать на основе этого кода и других данных код для компиляции с помощью метода, о котором написано выше, скомпилировать его, создать объект, определить пространство имен, с которым он будет ассоциирован и добавить это все дело в список аргументов. И вот здесь при реализации всего этого дела я столкнулся с еще одной проблемой. Дело в том, что в .Net Framework задача динамической компиляции решалась с помощью того же CodeDomProvider, но в .Net 5 и .Net Core такая возможность не поддерживается. То есть необходимые методы у этого класса вроде как есть, но они не реализованы. По этой причине для компиляции пришлось использовать инструменты Roslyn, ниже я опишу процесс, сейчас же я об этом написал, чтобы было понятно, что за объекты будут использоваться в коде. Метод, который здесь будет вызван для компиляции кода у меня возвращает кортеж (EmitResult, Assembly, String). Первый элемент этого кортежа содержит информацию, возвращенную компилятором, второй – скомпилированную сборку, третий – код, который был скомпилирован. С первыми двумя все понятно, но может возникнуть вопрос, зачем понадобилось сохранять код. Тут объяснение простое: обычно информация компилятора содержит данные о положении ошибки в коде. Но у нас кода как такового нет, он формируется «на лету» и положение ошибки не будет соответствовать ее положению в XSLT-документе, где она и будет расположена на самом деле. Все это будет сохраняться в специальном свойстве

    Dim _CompilationResultList As New List(Of (EmitResult, Assembly, String))

    Public ReadOnly Property CompilationResultList As List(Of (EmitResult, Assembly, String))

        Get

            Return _CompilationResultList

        End Get

    End Property

Ну и собственно сам код метода

    Private Sub AddExtObj(script As XElement)

        Dim refs = script.<msxsl:assembly>.Where(Function(asm) asm.@href IsNot Nothing).Select(Function(asm) asm.@href)

        Dim libpath = IO.Path.GetDirectoryName(GetType(String).Assembly.Location)

        refs.Concat(script.<msxsl:assembly>.Where(Function(asm) asm.@name IsNot Nothing).Select(Function(asm) Path.Combine(libpath, asm.@name)))

        Dim cresult = Compiler.CompileCode(GenerateCode(script), script.@language, refs.ToArray)

        _CompilationResultList.Add(cresult)

        If Not cresult.Item1.Success Then Exit Sub

        Dim obj = Activator.CreateInstance(cresult.Item2.GetType("Ns1.Class1"))

        Dim xmlns = script.GetNamespaceOfPrefix(script.Attribute("implements-prefix"))

        _ArgumentList.AddExtensionObject(xmlns.NamespaceName, obj)

    End Sub

 

Естественно, для того, чтобы все правильно искалось нужно импортировать необходимые пространства имен, в частности в этом коде используется следующее

Imports <xmlns:msxsl="urn:schemas-microsoft-com:xslt">

Добавление статического объекта

Кроме того, ради чего мы все это дело затеяли (возвращение поддержки внедренных скриптов), можно добавить еще пару-тройку возможностей, которые тоже не худо было бы иметь. На базе всего того, кода, что мы сейчас напишем это сделать совсем несложно.

Приведу простой пример. Помимо тех функций, которые мы могли бы написать с помощью скриптов или объектов расширения, неплохо было бы иметь возможность включать в скрипты уже существующие функции, которые изначально не предполагалось использовать в XSLT. Для примера можно взять математический функции, которых в первой версии XSLT так не хватает. Доступные нам «из коробки» математические функции определены в классе System.Math. И, казалось бы, что мешает использовать все это богатство напрямую. Но проблема в том, XsltArgumentList принимает объект, а вышеозначенный класс не позволяет создавать экземпляры, а все его методы – статические (Shared). То же самое можно сказать и о модулях Visual Basic, которые также содержать массу полезных функций.

Решить данную проблему можно выполнив следующие действия:

Сначала нам потребуется придумать собственное пространство имен для наших расширений. Можно, конечно, использовать и msxsl, добавив в него собственный функционал, но лучше этого не делать, дабы избежать возможных конфликтов. Я в своем примере добавил в XSLT вот такое пространство имен

xmlns:my="urn:my-xslt-extension-object-generator"

Затем включил в документ вот такой элемент

<my:static-object assembly-name="System.Runtime.dll" type="System.Math" implements-prefix="math"/>

Элемент содержит информацию о сборке типе и префиксе. Префикс мы будем использовать для вызова функций, таким образом нам понадобится объявить еще пространство имен для этого объекта расширения и ассоциировать его с этим префиксом.

xmlns:math="urn:my-xslt-extension-object-generator:math"

Пространство не обязательно должно быть именно таким, а вот префикс должен совпадать со значением атрибута implements-prefix. Если объектов будет несколько, то для функций каждого из них нужно будет создать свое пространство имен и вызывать их через свой префикс.

Теперь о коде. Во время загрузки документа нам потребуется обойти все эти элементы и обработать каждый из них специальным методом.

    Private Sub AddStaticObject(saticObj As XElement)

        Dim asmpath = Path.GetDirectoryName(GetType(System.Object).Assembly.Location)

        Dim asmFileName = saticObj.@<assembly-name> & If(saticObj.@<assembly-name>.EndsWith(".dll"), "", ".dll")

        Dim refs = New String() {If(saticObj.@<assembly-href>, Path.Combine(asmpath, asmFileName))}

        Dim proxygen As New ProxyGenerator

        Dim code = proxygen.GenerateStaticMethodsProxy(Assembly.LoadFile(refs(0)).GetType(saticObj.@type))

        Dim cresult = Compiler.CompileCode(code, "vb", refs)

        _CompilationResultList.Add(cresult)

        If Not cresult.Item1.Success Then Exit Sub

        Dim obj = Activator.CreateInstance(cresult.Item2.GetType((saticObj.@type).Split("."c).Last & "StaticProxy"))

        Dim xmlns = saticObj.GetNamespaceOfPrefix(saticObj.@<implements-prefix>)

        _ArgumentList.AddExtensionObject(xmlns.NamespaceName, obj)

    End Sub

 

Что здесь происходит. Для того, чтобы иметь возможность добавлять объект, нам потребуется прокси-класс, который мы создадим динамически. Для генерации кода класса мы создадим специальный класс ProxyGenerator, именно он здесь и используется. Код класса, который мы сгенерируем с его помощью будет: иметь возможность создавать экземпляры, содержать только (или в основном) методы, которые можно использовать в XSLT с его сильно ограниченной системой типов, а методы будут просто переадресовывать вызовы соответствующему статическому классу.

В своем проекте я других вариантов расширений не создавал, но так, если подумать, то, наверно, неплохо было бы еще создать расширение, которое позволило бы загружать динамически объект расширения из указанной сборки. Реализовать это несложно, но здесь я это описывать уже не буду.

Load и Transform

Метод Load у нас должен выполнять несколько действий. Во-первых, он должен сохранить документ XSLT, что вполне понятно. Во-вторых, следует обойти все скрипты и обработать их методом AddExtObj. В-третьих, то же самое нужно сделать со статическими объектами, равно как и с другими типами расширений, которые будут добавлены. Ну и наконец, нам нужно будет убедиться, что компиляция всех кодов прошла успешно, а в противном случае вызвать исключение.

Кроме того, нам понадобится поле для хранения загруженного документа

   Dim xslt As XDocument

 

    Public Sub Load(xsltPath As String)

        xslt = XDocument.Load(xsltPath)

        _ArgumentList = New XsltArgumentList

        _CompilationResultList.Clear()

        For Each script As XElement In xslt...<msxsl:script>

            AddExtObj(script)

        Next

        For Each stat As XElement In xslt...<my:static-object>

            AddStaticObject(stat)

        Next

 

        If Not _CompilationResultList.All(Function(r) r.Item1.Success) Then

            Throw New Exception($"Во время компиляции внедренных скриптов произошла были выявлены ошибки.

Подробности в списке {NameOf(CompilationResultList)} объекта {NameOf(XsltProcessor)}, вызвавшего исключение.")

        End If

    End Sub

Также желательно реализовать несколько перегрузок этого метода, но это уже чисто для удобства вызова.

Для метода Transform также неплохо было бы реализовать несколько перегрузок, наподобие тех, какие имеет класс XsltCompiledTransform. Но я здесь тоже покажу минимум того, что нужно.

    Public Sub Transform(input As XmlReader, output As TextWriter)

        Dim ctransform As New XslCompiledTransform(True)

        Dim cleandoc = New XDocument(xslt)

        cleandoc.Root.<msxsl:script>.Remove

        ctransform.Load(cleandoc.CreateReader)

        ctransform.Transform(input, _ArgumentList, output)

    End Sub

Здесь мы из сохраненного документа создаем копию и из этой копии нужно удалить все скрипты. Несмотря на то, что внедренные скрипты здесь не поддерживаются, XsltCompiledTransform знает о них и при добавлении объекта расширения, ассоциированного с тем же пространством имен, что и скрипт, он вываливает ошибку. Поэтому скрипты и нужно удалить. А вот удалять то, что мы сами придумали необязательно, поскольку неизвестные элементы здесь просто игнорируются. Хотя, на всяких случай можно сделать и это.

Компиляция

Как уже говорилось выше, для компиляции нам понадобятся инструменты Roslyn. Есть несколько пакетов NuGet, которые можно использовать, но для наших целей удобнее всего будет пакет Microsoft.CodeAnalysis.Compilers, он содержит компиляторы для обоих поддерживаемых языков (C# и VB.Net, если нужна поддержка других, придется искать другие инструменты) в одном пакете и ничего лишнего.

Код приведу полностью

Imports System.IO

Imports System.Runtime.Loader

Imports Microsoft.CodeAnalysis

Imports Microsoft.CodeAnalysis.CSharp

Imports cs = Microsoft.CodeAnalysis.CSharp

Imports Microsoft.CodeAnalysis.Text

Imports Microsoft.CodeAnalysis.VisualBasic

Imports vb = Microsoft.CodeAnalysis.VisualBasic

Imports System.Reflection

Imports Microsoft.CodeAnalysis.Emit

Imports System.Xml.XPath

Imports System.Xml

 

Public Class Compiler

 

    Private Shared Function GetCompilation(srcText As SourceText, lang As String, asmName As String, refs() As String) As Compilation

        Dim result As Compilation

        Dim reflist = refs.Select(Function(af) MetadataReference.CreateFromFile(af))

 

        Select Case lang.ToLower

            Case "vb", "visualbasic"

                Dim tree = vb.SyntaxFactory.ParseSyntaxTree(srcText)

                result = VisualBasicCompilation.Create(asmName, New SyntaxTree() {tree}, references:=reflist,

options:=New VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary,

                                           optimizationLevel:=OptimizationLevel.Release,

                                           assemblyIdentityComparer:=AssemblyIdentityComparer.Default))

            Case "cs", "csharp", "c#"

                Dim tree = cs.SyntaxFactory.ParseSyntaxTree(srcText)

                result = CSharpCompilation.Create(asmName, New SyntaxTree() {tree}, references:=reflist,

options:=New CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,

                           optimizationLevel:=OptimizationLevel.Release,

                           assemblyIdentityComparer:=AssemblyIdentityComparer.Default))

            Case Else

                Throw New NotSupportedException($"{lang} - Неизвестное определение языка. Поддерживаемые определения: vb, visualbasic, cs, csharp, c#")

        End Select

        Return result

    End Function

 

    Public Shared Function CompileCode(code As String, lang As String, references As IEnumerable(Of String)) As (EmitResult, Assembly, String)

        Dim asmreturn As Reflection.Assembly

        Using stream As New MemoryStream

            Dim srcCode = SourceText.From(code)

            Dim refNames = "System.Runtime System.Text.RegularExpressions System.Xml.XDocument System.Xml.ReaderWriter".Split(" "c)

            Dim refs = New List(Of String) From {

                Assembly.GetExecutingAssembly().Location,

                GetType(Object).Assembly.Location,

                GetType(XPathNodeIterator).Assembly.Location

            }

 

            refs.AddRange(references)

            refs.AddRange(refNames.Select(Function(astr) Assembly.Load(astr).Location))

            Dim compilation = GetCompilation(srcCode, lang, "temp.dll", refs.Distinct().ToArray)

            Dim result = compilation.Emit(stream)

            If (Not result.Success) Then

                Return (result, Nothing, code)

            End If

            stream.Seek(0, SeekOrigin.Begin)

            Dim asmLoadContext = New AssemblyLoadContext("temp.dll")

            asmreturn = asmLoadContext.LoadFromStream(stream)

            Return (result, asmreturn, code)

        End Using

    End Function

End Class

 

Здесь опять-таки, набор «референсов», загружаемый по умолчанию можно отредактировать по усмотрению или вынести это дело в конфигурацию проекта.

ProxyGenerator

 

При генерации кода прокси класса не имеет значения на каком языке его генерировать, поскольку результат не будет содержать пользовательского кода. И тут вроде нет особой необходимости использовать CodeDom для генерации, а вместо этого можно использовать простую генерацию текста. Но когда я попробовал сделать так, то с классом System.Math проблем не было, но вот когда я попытался тем же способом добавить модуль Microsoft.VisualBasic.Strings, возникли проблемы. Например ошибку вызвала вот эта функция Asc(String), все дело в том, что параметр этой функции имеет имя String, а это имя является ключевым словом языка и если оно используется в качестве идентификатора, должно заключаться в квадратные скобки. Таким образом получается, что для генерации нам понадобится как минимум список таких слов и при использовании имен придется выполнять их проверку. Если же использовать генерацию на основе CodeDom, эти проблемы будут решены автоматически, да и вообще мало ли какие еще проблемы могли бы возникнуть.

Итак, нам нужно создать класс, в котором для каждого статического метода будут формироваться что-то подобное.  Если исходный код выглядит так

Public Function Asc (String As String) As Integer

 

То результат должен быть следующим

    Public Function Asc([String] As String) As Integer

        Return Microsoft.VisualBasic.Strings.Asc([String])

    End Function

И так для каждого метода.

Помимо этого, нам нужно будет решить еще пару задач. Нам необходимо будет отфильтровать методы класса, поскольку далеко не все что может быть в CLR-типах пригодно для использования в XSLT. В первую очередь проблема в системах типов. XSLT поддерживает ограниченный набор типов. Для первой версии (с которой мы и работаем) это: числа, строки, «булины», узлы и наборы узлов (кажись все). Узлы в коде функций будут представлены типом System.Xml.XPath.XPathNavigator , а наборы узлов - System.Xml.XPath.XPathNodeIterator. Точнее производными классами, поскольку эти абстрактны. Естественно нас интересуют только те функции, у которых типы параметров и возвращаемого значения совместимы с данным набором. Это могут быть примитивы, строки, перечисления и вышеозначенные типы, представляющие узлы и их наборы. Таким образом в оригинальных типах нам нужно будет отфильтровать методы.

Другой момент, это то, что среди методов, возвращающих «неудобные» типы могут оказаться и довольно полезные. Ну например - в том же модуле Strings есть довольно полезная функция Split, реализация которой средствами XSLT – та еще проблема. Но вот что потом делать с результатом? В XSLT первой версии нет средств для работы с массивом строк, то есть его потом просто не будет возможности разобрать на составляющие. И вот для таких полезных методов можно предусмотреть конвертеры. В частности, коллекции строк можно заменять наборами текстовых узлов, с которыми уже можно работать.

Код генератора:

Imports System.CodeDom

Imports System.Reflection

Imports System.Text

Imports System.Xml.XPath

Imports eo = IncludedScriptSupport.ExtensionObjects

Public Class ProxyGenerator

 

    Private Function GenerateMethodDom(mi As MethodInfo) As CodeMemberMethod

        Dim result As New CodeMemberMethod()

        With result

            .Name = mi.Name

            .Attributes = MemberAttributes.Public

            .Parameters.AddRange(

            New CodeParameterDeclarationExpressionCollection(

                mi.GetParameters().

                Select(Function(pi) New CodeParameterDeclarationExpression(pi.ParameterType, pi.Name)).ToArray))

            .ReturnType = New CodeTypeReference(If(mi.ReturnType.IsAssignableTo(GetType(IEnumerable(Of String))), GetType(XPathNodeIterator), mi.ReturnType))

            Dim typeref = New CodeTypeReferenceExpression(mi.DeclaringType)

            Dim method As New CodeMethodReferenceExpression(typeref, mi.Name)

            Dim params = mi.GetParameters().Select(Function(pi) New CodeVariableReferenceExpression(pi.Name))

            Dim inv As New CodeMethodInvokeExpression(method, params.ToArray)

            If mi.ReturnType.IsAssignableTo(GetType(IEnumerable(Of String))) Then

                inv = AddStringListConverter(inv)

            End If

 

            .Statements.Add(New CodeMethodReturnStatement(inv))

        End With

        Return result

    End Function

 

    Private Function AddStringListConverter(expr As CodeMethodInvokeExpression) As CodeMethodInvokeExpression

        Dim method As New CodeMethodReferenceExpression(New CodeTypeReferenceExpression(GetType(eo.Helpers)), NameOf(eo.Helpers.SListToNodeset))

        Dim result As New CodeMethodInvokeExpression(method, expr)

        Return result

    End Function

 

    Private Function GenerateProxyClassDom(t As Type) As CodeTypeDeclaration

        Dim types = New Type() {

            GetType(String),

            GetType(XPathNavigator),

            GetType(XPathNodeIterator),

            GetType(IEnumerable(Of String)),

            GetType([Enum])}

        Dim methods = t.GetMethods(BindingFlags.Public Or BindingFlags.Static).

            Where(Function(mi)

                      Return mi.GetParameters().Select(Function(pi) pi.ParameterType).Append(mi.ReturnType).

                      All(Function(mt) mt.IsPrimitive OrElse types.Any(Function(ct) mt.IsAssignableTo(ct)))

                  End Function)

        Dim typedecl As New CodeTypeDeclaration(t.Name & "StaticProxy")

        typedecl.Members.AddRange((From mi As MethodInfo In methods Select GenerateMethodDom(mi)).ToArray)

        Return typedecl

    End Function

 

    Public Function GenerateStaticMethodsProxy(t As Type) As String

        Dim sb As New StringBuilder

        Using writer As New IO.StringWriter(sb)

            Dim provider = CodeDom.Compiler.CodeDomProvider.CreateProvider("VB")

            provider.GenerateCodeFromType(GenerateProxyClassDom(t), writer, Nothing)

        End Using

        Return sb.ToString()

    End Function

End Class

 

По поводу конвертера. Собственно, сам конвертер можно либо внедрить в сборку, либо реализовать в другой сборке, но в этом случае при компиляции кода надо будет дать ссылку на эту сборку. Я реализовал конвертер в самом проекте и при компиляции добавляю ссылку на него.

Imports System.Xml.XPath

 

Namespace ExtensionObjects

    Public Class Helpers

        Public Shared Function SListToNodeset(slist As IEnumerable(Of String)) As XPathNodeIterator

            Return <r><%= From ss As String In slist Select <a><%= ss %></a> %></r>.ToXPathNavigable().CreateNavigator().Select(".//text()")

        End Function

 

    End Class

 

End Namespace

Здесь я импортировал пространство System.Xml.XPath для того, чтобы были доступны определенные в этом пространстве методы расширения для XNode, нас интересует здесь метод ToXPathNavigable(), чтобы получить навигатор для узла и получить набор узлов с помощью метода Select. То есть здесь я просто создал фрагмент документа, содержащий строки коллекции и выбрал их как узлы.

Результаты

Приложение я сделал предельно простым: на форме кнопка и текстовое поле. Нажимаем на кнопку и выполняется преобразование и в текстовом поле появляется либо результат, либо список ошибок, если все прошло неудачно. Код кнопки такой

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim proc As New XsltProcessor()

        Dim input = XDocument.Load(IO.Path.Combine(Application.StartupPath, "XMLFile1.xml"))

        Try

            proc.Load(IO.Path.Combine(Application.StartupPath, "XSLTFile1.xslt"))

        Catch ex As Exception

            TextBox1.Text = ex.ToString

            For Each res In proc.CompilationResultList

                TextBox1.AppendText(res.Item3)

                TextBox1.AppendText(String.Join(vbCrLf & vbCrLf, res.Item1.Diagnostics.Select(Function(d) d.ToString)))

            Next

            Exit Sub

        End Try

        Dim sb As New StringBuilder

        Using writer = New IO.StringWriter(sb)

            proc.Transform(input.CreateReader, writer)

        End Using

        TextBox1.Text = (sb.ToString)

    End Sub

Тестировалось все на следующем примере. Входной документ

<?xml version="1.0" encoding="utf-8" ?>

<input>

       <pow base="5" exp="3"/>

       <dblstr>ma</dblstr>

       <mid start="9" length="5">This is input string</mid>

       <split>string1,string2,string3</split>

</input>

 

XSLT

<?xml version="1.0" encoding="utf-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl s1 s2 my math str"

xmlns:s1="urn:included-script:s1"

xmlns:s2="urn:included-script:s2"

xmlns:my="urn:my-xslt-extension-object-generator"

xmlns:math="urn:my-xslt-extension-object-generator:math"

xmlns:str="urn:my-xslt-extension-object-generator:strings"

                                                      >

    <xsl:output method="xml" indent="yes"/>

 

    <xsl:template match="@* | node()">

        <xsl:copy>

            <xsl:apply-templates select="@* | node()"/>

        </xsl:copy>

    </xsl:template>

 

       <xsl:template match="/">

             <result>

                    <pow>

                           <xsl:value-of select="s1:pow(*/pow/@base, */pow/@exp)"/>

                    </pow>

                    <dblstr>

                           <xsl:value-of select="s2:dblstr(*/dblstr/text())"/>

                    </dblstr>

                    <exp>

                           <xsl:value-of select="math:Exp(1)"/>

                    </exp>

                    <mid>

                           <xsl:value-of select="str:Mid(*/mid/text(), */mid/@start, */mid/@length)"/>

                    </mid>

                    <split>

                           <xsl:for-each select="str:Split(*/split/text(), ',', -1, 1)">

                                  <fragment>

                                        <xsl:value-of select="."/>

                                  </fragment>

                           </xsl:for-each>

                    </split>

             </result>

       </xsl:template>

 

       <my:static-object assembly-name="System.Runtime.dll" type="System.Math" implements-prefix="math"/>

       <my:static-object assembly-name="Microsoft.VisualBasic.Core" type="Microsoft.VisualBasic.Strings" implements-prefix="str"/>

 

       <msxsl:script implements-prefix="s1" language="visualbasic">

             <![CDATA[

             Public Function pow(base As Double, exp As Double)

                    Return Math.Pow(base, exp)

             End Function

             ]]>

       </msxsl:script>

 

       <msxsl:script implements-prefix="s2" language="csharp">

             <![CDATA[

             public string dblstr(string input)

             {

                    return input + input;

             }

             ]]>

       </msxsl:script>

 

</xsl:stylesheet>

 

Здесь мы видим в коде преобразования два скрипта, на двух поддерживаемых языках, кроме того два импортированных статических объекта (Math и Strings) и само преобразование использует функции как из скриптов, так и из импортированных объектов.

Результат:

<?xml version="1.0" encoding="utf-16"?>
<
result>
       <
pow>125</pow>
       <
dblstr>mama</dblstr>
       <
exp>2.718281828459045</exp>
       <
mid>input</mid>
       <
split>
             <fragment>string1</fragment>
             <
fragment>string2</fragment>
             <fragment>string3</fragment>
       </split>

</result>

 

IncludedScriptSupport.zip

Комментариев нет :

Отправить комментарий