Dynamic XSD + Linq
Я заметил, что у меня на сайте опубликованы десятки моих различных SQL-процедур, сделанных на XSLT, различных SQL-CLR-сборок, сделаных на XSLT, всякие хитрые движки на XSLT, многочисленные шлюзы, разбирающие XML, но совсем мало примеров на LINQ. Поэтому я выложу тут еще одну версию той же формы, что я показал выше. Эту форму я переделывал больше десятка раз. Первую версию - на простом классе, когда предполагались четко зафиксированные набор свойств, была фиксированная схема XML и по существу могли меняться только данные (значения XML-атрибутов) - я показал выше. Потом я переделал эту же форму на LINQ - в условиях когда атрибуты, теги и пространства имен были изначально известны и четко определены. Предполагалось, что XML-схема меняться не будет. Это позволило загрузить XSD-схему в каталог проекта Visual Studio, указать директиву Import и работать на LINQ с прекрасной подсказкой (небольшой фрагмент кода из этого варианта я показал выше).
Однако позже выяснилось, что схема может меняться администратором системы. От статической подсказки Visual Studio пришлось отказаться. Пришлось отказаться и от сервиса компилятора VB, который автоматически умеет добавлять пространство имен, указанное в Import. Потом выяснилось, что некие атрибуты у некоторых тегов должны все же быть всегда и админам системы нельзя позволять менять схему произвольно. Потом выяснилось, что и имя схемы может иногда меняться, потом выяснилось что эта же форма должна работать с несколькими разными XML-полями в SQL и так далее и так далее - уже изготовлено более 10 вариантов этой формы.
Поэтому я решил показать тут один промежуточный вариант этой формы на LINQ, в котором начинающие программисты могут увидеть для себя много поучительного. Этот вариант состоит из трех форм и работает с динамически формируемой админом системы XSD-схемой с фиксированным пространством имен, но работающий с несколькими различными XML-полями в базе. При этом пользователи системы могут вносить данные по схеме, определенной админом системы, имя схемы определено статически, но никаких изначальных ограничений на построение админом схемы данных этот вариант кода не предусматривает. Я выбрал для показа на сайте именно этот вариант (из более чем десяти вариантов), потому что именно этот вариант кода наиболее легко гнется в любую сторону.
Итак, у меня в базе создано семь XML-полей с профилями. Админ создает схемы данных, хранящихся в этих профилях, потом юзера вносят данные по этим схемам. Здесь будет рассмотрена лишь часть админа, который имеет возможность создать XML-схемы хранения данных в семи XML-полях MS SQL-сервера, и заполнить данными созданные схемы.
Это можно было бы решить множеством способов, но я предпочитаю всегда наиболее толстый SQL-слой из вохможных, поэтому у меня есть в SQL несколько процедур, одна из которых все читает из SQL, другие сохраняют XML в поля базы (надеюсь вы узнаете дизайнер LINQ)
В проекте существует форма, которая к делу не относится, но позволяет манипулироваться с помощью Querystring("Type") типом SQL-полей. Нужная нам форма SetUserPaymentProfile, которая позволяет произвольно добавлять и убавлять теги в Sequence-последовательностях. В данном случае пока есть только тег A, а теперь c помощью формы SetPaymentProfileTags добавлен тег B и теперь добавлен еще тег C.
Далее c помощью формы SetPaymentProfileSchema мы можем добавить атрибуты к тегам, в которых можно будет укладывать данные. Добавим атрибут C1 (и сразу пропишем в него значение 111) и атрибут С2 и сразу пропишем в него значение 222. В итоге получаем данные чрезвычайно гибкой структуры (практически произвольной), которые укладываются в базу.
Итак, как же достигается такое чудо - мы можем хранить полностью нерегулярные данные по практически произвольной схеме, компонуемой прямо по ходу работы системы?
Тело формы SetUserPaymentProfile, которая отображает итоговой XML и позволяет добавлять/удалять дочерние ноды выглядит вот так (фрагмент):
15: <blockquote>
16: <table width="100%" style="text-align: left; vertical-align: top;">
17: <tr>
18: <td>
19: <h4>
20: <asp:Label ID="lTitle" runat="server"></asp:Label>
21: </h4>
22: </td>
23: </tr>
24: <tr>
25: <td>
26: <asp:Panel ID="Panel1" runat="server" ScrollBars="Vertical" Height="350">
27: <asp:DataList ID="DataList1" runat="server">
28: <HeaderTemplate>
29: <td>
30: </td>
31: <td>
32: </td>
33: <td>
34: </td>
35: </HeaderTemplate>
36: <ItemTemplate>
37: <td>
38: <asp:ImageButton ID="DelImageButton1" runat="server" ImageUrl="~/Images/remove.gif"
39: OnClick="DelImageButton1_Click" CommandArgument='<%# Databinder.Eval(Container,"ItemIndex") %>' />
40: <asp:Literal ID="AddAttrLiteral" runat="server"></asp:Literal>
41: </td>
42: <td>
43: <asp:Label ID="tx_Tag_Name" runat="server"></asp:Label>
44: </td>
45: <td>
46: <asp:TextBox ID="tx_Xml" runat="server" Width="550" ReadOnly="true"></asp:TextBox>
47: </td>
48: </ItemTemplate>
49: </asp:DataList>
50: <asp:ImageButton ID="RefreshImageButton1" runat="server" ImageUrl="~/Images/ico0016.gif"
51: OnClick="RefreshImageButton1_Click" />
52: <asp:Literal ID="AddNodesLiteral" runat="server"></asp:Literal>
53: </asp:Panel>
54: </td>
55: </tr>
56: <tr>
57: <td>
58: <asp:Button ID="Button1" runat="server" Text="OK" BackColor="#EF6771" Width="130px" />
59: </td>
60: </tr>
61: </table>
62: <asp:Label ID="Lerr1" runat="server" ForeColor="Red"></asp:Label>
63: </blockquote>...
4: Partial Class SetUserPaymentProfile
5: Inherits ProfileBasePage2
6:
7: Protected Sub SetUserPaymentProfile_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
8: 'Navigator1.NavigateList( "MyRequest", "MyRequestLog.aspx", "Exit", "Logout.ashx")
9: '
10: Select Case Request.QueryString("Type")
11: Case "BankingCards"
12: lTitle.Text = "Банковские карты. Профиль "
13: Case "InetMoney"
14: lTitle.Text = "Виртуальные деньги. Профиль "
15: Case "HardMoney"
16: lTitle.Text = "Наличные. Профиль "
17: Case "Invoice"
18: lTitle.Text = "Инвойс. Профиль "
19: Case "BankTransfer"
20: lTitle.Text = "Банковский перевод. Профиль "
21: Case "Bonus"
22: lTitle.Text = "Бонусные баллы. Профиль "
23: Case "Reserved"
24: lTitle.Text = "Отложенная оплата. Профиль "
25: Case "Common"
26: lTitle.Text = "Общий профиль "
27: End Select
28: '
29: AddNodesLiteral.Text = "<a onclick=""window.open('','new','resizable=no,menubar=no,scrollbars=no,width=800,height=500');"" target='new' title='new' href='SetPaymentProfileTags.aspx?type=" & Request.QueryString("Type") & "&id=" & Me.TargetUser.id & "'><img src='Images/ico0015.gif' style='border-width:0'/></a>"
30: If Not IsPostBack Then
31: Try
32: lTitle.Text &= " " & Me.TargetUserPaymentProfile(0).ProfileName
33: Refresh()
34: '
35: Catch ex As Exception
36: DebugLog.TraceTXT(Me.AppRelativeVirtualPath, ex.Message)
37: Lerr1.Text = ex.Message
38: End Try
39: End If
40: End Sub
41:
42: Protected Sub RefreshImageButton1_Click(ByVal sender As Object, ByVal e As System.Web.UI.ImageClickEventArgs) Handles RefreshImageButton1.Click
43: Refresh()
44: End Sub
45:
46: Sub Refresh()
47: Dim Nodes As Collections.Generic.IEnumerable(Of XNode) = WorkingXML.Nodes()
48: DataList1.DataSource = Nodes
49: DataList1.DataBind()
50: End Sub
51:
52:
53: Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
54: If Me.CurrentUser Is Nothing Then Exit Sub 'прокисла сессия
55: If Me.TargetUser Is Nothing Then Exit Sub 'прокисла сессия
56: '
57: If Me.CurrentUser.IsAdmin Then
58: Try
59: Me.SaveProfile()
60: '
61: Response.Write("<script type='text/javascript' language='javascript'>" & vbCrLf & _
62: " window.close();" & vbCrLf & _
63: "</script>")
64: Response.End()
65: '
66: Catch ex As Exception
67: DebugLog.TraceTXT(Me.AppRelativeVirtualPath, ex.Message)
68: Lerr1.Text = ex.Message
69: End Try
70: End If
71: '
72: End Sub
73:
74: Protected Sub DataList1_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DataListItemEventArgs) Handles DataList1.ItemDataBound
75: If e.Item.DataItem IsNot Nothing Then
76: Dim OneXmlTag As XElement = CType(e.Item.DataItem, XElement)
77: '<b:VISA xmlns:b="http://Airts.vb-net.com/" b:Module="Assist" b:Code="11" b:Comission="22" b:DayLimit="33" b:MounthLimit="44" />
78: '
79: Dim tx_Tag_Name As Label = CType(e.Item.FindControl("tx_Tag_Name"), Label)
80: tx_Tag_Name.Text = OneXmlTag.Name.LocalName
81: '
82: Dim tx_xml As TextBox = CType(e.Item.FindControl("tx_xml"), TextBox)
83: tx_xml.Text = OneXmlTag.ToString
84: '
85: Dim AddAttrLiteral As Literal = CType(e.Item.FindControl("AddAttrLiteral"), Literal)
86: AddAttrLiteral.Text = "<a onclick=""window.open('','edit','resizable=no,menubar=no,scrollbars=no,width=800,height=500');"" target='edit' title='edit' href='SetPaymentProfileSchema.aspx?type=" & Request.QueryString("Type") & "&id=" & Me.TargetUser.id & "&Node=" & OneXmlTag.Name.LocalName & "'><img src='Images/ico0008.gif' style='border-width:0'/></a>"
87: End If
88: End Sub
89:
90: Protected Sub DelImageButton1_Click(ByVal sender As Object, ByVal e As System.Web.UI.ImageClickEventArgs)
91: Dim DelImageButton1 As ImageButton = CType(sender, ImageButton)
92: Dim BankingCards_xml As Collections.Generic.IEnumerable(Of XElement) = From AllColumn In Me.TargetUserPaymentProfile Select AllColumn.BankingCardsProfile
93: If BankingCards_xml(0) IsNot Nothing Then
94: Dim Nodes As Collections.Generic.IEnumerable(Of XNode) = BankingCards_xml(0).Nodes()
95: Nodes(DelImageButton1.CommandArgument).Remove()
96: End If
97: Me.TargetUserPaymentProfile(0).BankingCardsProfile = BankingCards_xml(0)
98: Refresh()
99: End Sub
100:
101: End Class
Как видите, эта форма не делает почти ничего. Отрисовывает табличку, формирует код для кнопок обновить, добавить и удалить ноды. И обрабатывает эти события. Все остальное вынесено в базовую страничку проекта, которую я опубликовал в разделе Базовые странички. А в этой самой базовой страничке и выполняются все операции LINQ.
Увы, XElement несеализуем, поэтому запихнуть его во вьюстейт не получится по-простому. Поэтому я нашел оптимальное решение - чтобы хранить его в Session, но с разными именами. Тогда множество отдельных форм, открытых админом на экране для разных юзеров - будут работать с разными вариантами рабочего профиля юзера.
Форма SetPaymentProfileTags для манипулирования тегами устроена так же просто. В Диве работает размещена табличка, отображающая последовательность тегов. Теги можно добавлять, убирать и редактировать атрибуты. Это как раз и есть работа с типизированными коллекциями LINQ.
1: <%@ Page Language="VB" AutoEventWireup="false" CodeFile="SetPaymentProfileTags.aspx.vb"
2: Inherits="SetPaymentProfileTags" EnableEventValidation="false" ValidateRequest="false" %>
3:
4: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
5: <html xmlns="http://www.w3.org/1999/xhtml">
6: <body style="background-color: #E5E9EC;">
7: <form id="form1" runat="server">
8: <blockquote>
9: <asp:Panel ID="Panel1" runat="server" ScrollBars="Vertical" Height="400">
10: <asp:Label ID="lTitle" runat="server" Font-Bold="true"></asp:Label>
11: <br />
12: <br />
13: <asp:DataList ID="DataList1" runat="server" Width="100%" Style="text-align: left;
14: vertical-align: middle;">
15: <ItemTemplate>
16: <tr>
17: <td>
18: <asp:ImageButton ID="DelImageButton1" runat="server" ImageUrl="~/Images/remove.gif"
19: OnClick="DelImageButton1_Click" CommandArgument='<%# Databinder.Eval(Container,"ItemIndex") %>' />
20: </td>
21: <td>
22: <asp:TextBox ID="tx_Name" runat="server" Text='<%# Eval("Name") %>' Width="300"></asp:TextBox>
23: </td>
24: </tr>
25: </ItemTemplate>
26: </asp:DataList>
27: <br />
28: <table width="100%" style="text-align: left; vertical-align: middle;">
29: <tr>
30: <td>
31: <asp:ImageButton ID="AddImageButton1" runat="server" ImageUrl="~/Images/ico0015.gif" />
32: </td>
33: <td>
34: <asp:TextBox ID="tx_Name" runat="server" Width="300"></asp:TextBox>
35: </td>
36: </tr>
37: </table>
38: </asp:Panel>
39: <br />
40: <asp:Button ID="Button1" runat="server" Text="OK" Width="130px" BackColor="#EF6771" /><br />
41: <asp:Label ID="Lerr1" runat="server" ForeColor="Red"></asp:Label>
42: </blockquote>
43: </form>
44: </body>
45: </html>
2:
3: Partial Class SetPaymentProfileTags
4: Inherits ProfileBasePage2
5:
6: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
7: Try
8: lTitle.Text = Request.QueryString("Type") & "=> Tags" & " (Профиль " & Me.TargetUserPaymentProfile(0).ProfileName & ")"
9: If Not IsPostBack Then
10: refresh()
11: End If
12: Catch ex As Exception
13: DebugLog.TraceTXT(Me.AppRelativeVirtualPath, ex.Message)
14: Lerr1.Text = ex.Message.Replace("<", "<").Replace(">", ">")
15: End Try
16:
17: End Sub
18:
19: Sub refresh()
20: If Me.WorkingXML IsNot Nothing Then
21: DataList1.DataSource = Me.WorkingXML.Nodes
22: DataList1.DataBind()
23: End If
24: End Sub
25:
26: Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
27: '
28: Me.SaveProfile()
29: '
30: Response.Write("<script type=""text/javascript"" language=""javascript""> " & vbCrLf & _
31: "window.close();" & vbCrLf & _
32: "</script>")
33: End Sub
34:
35: Protected Sub AddImageButton1_Click(ByVal sender As Object, ByVal e As System.Web.UI.ImageClickEventArgs) Handles AddImageButton1.Click
36: Try
37: Dim NewNodes As New XElement("{http://Airts.vb-net.com/}" & tx_Name.Text)
38: Me.WorkingXML.Add(NewNodes)
39: tx_Name.Text = ""
40: refresh()
41: Catch ex As Exception
42: Lerr1.Text = ex.Message
43: End Try
44:
45: End Sub
46:
47: Protected Sub DelImageButton1_Click(ByVal sender As Object, ByVal e As System.Web.UI.ImageClickEventArgs)
48: Dim DelImageButton1 As ImageButton = CType(sender, ImageButton)
49: Try
50: If Me.WorkingXML.Nodes IsNot Nothing Then
51: Dim NewNodes As XElement = Me.WorkingXML.Nodes(DelImageButton1.CommandArgument)
52: NewNodes.Remove()
53: tx_Name.Text = ""
54: End If
55: refresh()
56: Catch ex As Exception
57: Lerr1.Text = ex.Message
58: End Try
59: End Sub
60: End Class
И наконец последняя (тоже всплывающая) форма, которая позволяет манипулировать атрибутами каждого тега. Она устроена так же просто - добавляем, удаляем атрибуты (и их значения).
1: <%@ Page Language="VB" AutoEventWireup="false" CodeFile="SetPaymentProfileSchema.aspx.vb"
2: Inherits="SetPaymentProfileSchema" EnableEventValidation="false" ValidateRequest="false" %>
3:
4: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
5: <html xmlns="http://www.w3.org/1999/xhtml">
6: <body style="background-color: #E5E9EC;">
7: <form id="form1" runat="server">
8: <blockquote>
9: <asp:Panel ID="Panel1" runat="server" ScrollBars="Vertical" Height="400">
10: <asp:Label ID="lTitle" runat="server" Font-Bold="true"></asp:Label>
11: <br />
12: <br />
13: <asp:DataList ID="DataList1" runat="server" Width="100%" Style="text-align: left;
14: vertical-align: middle;">
15: <ItemTemplate>
16: <tr>
17: <td>
18: <asp:ImageButton ID="DelImageButton1" runat="server" ImageUrl="~/Images/remove.gif"
19: OnClick="DelImageButton1_Click" CommandArgument='<%# Eval("Name") %>' />
20: </td>
21: <td>
22: <asp:TextBox ID="tx_Name" runat="server" Text='<%# Eval("Name") %>' Width="300"></asp:TextBox>
23: </td>
24: <td>
25: <asp:TextBox ID="tx_Value" runat="server" Text='<%# Eval("Value") %>' Width="300"></asp:TextBox>
26: </td>
27: </tr>
28: </ItemTemplate>
29: </asp:DataList>
30: <br />
31: <table width="100%" style="text-align: left; vertical-align: middle;">
32: <tr>
33: <td>
34: <asp:ImageButton ID="AddImageButton1" runat="server" ImageUrl="~/Images/ico0015.gif" />
35: </td>
36: <td>
37: <asp:TextBox ID="tx_Name" runat="server" Width="300"></asp:TextBox>
38: </td>
39: <td>
40: <asp:TextBox ID="tx_Value" runat="server" Width="300"></asp:TextBox>
41: </td>
42: </tr>
43: </table>
44: </asp:Panel>
45: <br />
46: <asp:Button ID="Button1" runat="server" Text="OK" Width="130px" BackColor="#EF6771" /><br />
47: <asp:Label ID="Lerr1" runat="server" ForeColor="Red"></asp:Label>
48: </blockquote>
49: </form>
50: </body>
51: </html>
3: Partial Class SetPaymentProfileSchema
4: Inherits ProfileBasePage2
5:
6: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
7: Try
8: lTitle.Text = Request.QueryString("Type") & "=> " & Request.QueryString("Node") & "=> " & "Attributes" & " (Профиль " & Me.TargetUserPaymentProfile(0).ProfileName & ")"
9: If Not IsPostBack Then
10: refresh()
11: End If
12: Catch ex As Exception
13: DebugLog.TraceTXT(Me.AppRelativeVirtualPath, ex.Message)
14: Lerr1.Text = ex.Message.Replace("<", "<").Replace(">", ">")
15: End Try
16:
17: End Sub
18:
19: Sub refresh()
20: If Me.WorkingXML IsNot Nothing Then
21: Dim WorkAttibutes As System.Collections.Generic.IEnumerable(Of XAttribute) = GetWorkingNode(Me.WorkingXML).Attributes
22: DataList1.DataSource = WorkAttibutes
23: DataList1.DataBind()
24: End If
25: End Sub
26:
27: Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
28: '
29: Me.SaveProfile()
30: '
31: Response.Write("<script type=""text/javascript"" language=""javascript""> " & vbCrLf & _
32: "window.close();" & vbCrLf & _
33: "</script>")
34: End Sub
35:
36: Protected Sub AddImageButton1_Click(ByVal sender As Object, ByVal e As System.Web.UI.ImageClickEventArgs) Handles AddImageButton1.Click
37: Try
38: If Me.WorkingXML IsNot Nothing Then
39: Dim NewAttr As New XAttribute("{http://Airts.vb-net.com/}" & tx_Name.Text.Trim, tx_Value.Text.Trim)
40: GetWorkingNode(Me.WorkingXML).Add(NewAttr)
41: tx_Name.Text = ""
42: tx_Value.Text = ""
43: End If
44: refresh()
45: Catch ex As Exception
46: Lerr1.Text = ex.Message
47: End Try
48:
49: End Sub
50:
51: Protected Sub DelImageButton1_Click(ByVal sender As Object, ByVal e As System.Web.UI.ImageClickEventArgs)
52: Dim DelImageButton1 As ImageButton = CType(sender, ImageButton)
53: Try
54: If Me.WorkingXML IsNot Nothing Then
55: Dim NewAttr As XAttribute = GetWorkingNode(Me.WorkingXML).Attribute(DelImageButton1.CommandArgument)
56: NewAttr.Remove()
57: tx_Name.Text = ""
58: tx_Value.Text = ""
59: End If
60: refresh()
61: Catch ex As Exception
62: Lerr1.Text = ex.Message
63: End Try
64: End Sub
65:
66: Function GetWorkingNode(ByVal Xml As XElement) As XElement
67: If Xml IsNot Nothing Then
68: Dim WorkingNode As XElement
69: For i As Integer = 0 To Xml.Nodes().Count - 1
70: If CType(Xml.Nodes(i), XElement).Name.LocalName = Request.QueryString("Node") Then
71: WorkingNode = CType(Xml.Nodes(i), XElement)
72: End If
73: Next
74: Return WorkingNode
75: End If
76: End Function
77:
78: End Class
Как видите, буквально несколько десятков строк кода - а какая получилась красота! Это гораздо круче например движка 1С, который тоже делает нечто подобное - только он не умеет организовать ДЛЯ КАЖДОГО документа собственные реквизиты (свойства) - а только для всей последовательности. В моей концепции - КАЖДЫЙ документ имеет самостоятельные реквизиты. И достигнуто это буквально несколькими строчками кода на бейсике.
Кроме того, если вы заметили петлю на дизайнере LINQ - все докуметы выстроены в иерархическое дерево. То есть нижестоящие документы наследуют реквизиты предшствующих докуметов. Это тоже достигается буквально несколькими строками бейсика. Но это уже другая концепция, никак с описывамым тут LINQ-подходом не связанная.
<SITEMAP> <MVC> <ASP> <NET> <DATA> <KIOSK> <FLEX> <SQL> <NOTES> <LINUX> <MONO> <FREEWARE> <DOCS> <ENG> <CHAT ME> <ABOUT ME> < THANKS ME> |