deploy: 81ffc9da10
This commit is contained in:
parent
158b902b20
commit
cb4e8e2194
131 changed files with 3191 additions and 3076 deletions
|
|
@ -2,19 +2,19 @@
|
|||
<head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<meta name=generator content="Hugo 0.89.0">
|
||||
<meta name=generator content="Hugo 0.89.1">
|
||||
<meta name=description content>
|
||||
<title>Cross Session Events :: BC AL Help</title>
|
||||
<link href=/css/nucleus.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/fontawesome-all.min.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/featherlight.min.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/perfect-scrollbar.min.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/auto-complete.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/theme.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/theme-blue.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/variant.css?1636104707 rel=stylesheet>
|
||||
<link href=/css/print.css?1636104707 rel=stylesheet media=print>
|
||||
<script src=/js/jquery.min.js?1636104707></script>
|
||||
<link href=/css/nucleus.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/fontawesome-all.min.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/featherlight.min.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/perfect-scrollbar.min.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/auto-complete.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/theme.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/theme-blue.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/variant.css?1636386789 rel=stylesheet>
|
||||
<link href=/css/print.css?1636386789 rel=stylesheet media=print>
|
||||
<script src=/js/jquery.min.js?1636386789></script>
|
||||
<style>:root #header+#content>#left>#rlblock_left{display:none!important}</style>
|
||||
</head>
|
||||
<body data-url=/navpatterns/1-patterns/cross-session-events/>
|
||||
|
|
@ -32,9 +32,9 @@ BC AL Help . dev
|
|||
<input data-search-input id=search-by type=search placeholder=Search...>
|
||||
<span data-search-clear><i class="fas fa-times"></i></span>
|
||||
</div>
|
||||
<script src=/js/lunr.min.js?1636104707></script>
|
||||
<script src=/js/auto-complete.js?1636104707></script>
|
||||
<script src=/js/search.js?1636104707></script>
|
||||
<script src=/js/lunr.min.js?1636386789></script>
|
||||
<script src=/js/auto-complete.js?1636386789></script>
|
||||
<script src=/js/search.js?1636386789></script>
|
||||
</div>
|
||||
<div class=highlightable>
|
||||
<ul class=topics>
|
||||
|
|
@ -200,9 +200,6 @@ BC AL Help . dev
|
|||
<div class=wrapper>
|
||||
<nav id=TableOfContents>
|
||||
<ul>
|
||||
<li>
|
||||
<ul>
|
||||
<li></li>
|
||||
<li><a href=#abstract>Abstract</a></li>
|
||||
<li><a href=#problem>Problem</a></li>
|
||||
<li><a href=#solution>Solution</a></li>
|
||||
|
|
@ -210,8 +207,6 @@ BC AL Help . dev
|
|||
<li><a href=#consequences>Consequences</a></li>
|
||||
<li><a href=#related-topics>Related Topics</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -220,15 +215,14 @@ BC AL Help . dev
|
|||
</div>
|
||||
<main id=body-inner>
|
||||
<h1>Cross Session Events</h1>
|
||||
<h4 id=_by-nikolai-lestrange-from-tvision-technology-ltd-in-the-uk__><em>By Nikolai L’Estrange, from TVision Technology Ltd. in the UK</em>_</h4>
|
||||
<p>_</p>
|
||||
<h3 id=abstract>Abstract</h3>
|
||||
<p><em>By Nikolai L’Estrange, from TVision Technology Ltd. in the UK</em></p>
|
||||
<h2 id=abstract>Abstract</h2>
|
||||
<p>Track things that happen in other NAV Sessions.</p>
|
||||
<p><a href=PubSub.png><img src=PubSub.png alt=" "></a></p>
|
||||
<h3 id=problem>Problem</h3>
|
||||
<h2 id=problem>Problem</h2>
|
||||
<p>In Microsoft Dynamics NAV you can fire a function whenever something changes within your session (and from NAV 2016 this is even easier with the new Event model), however there is not an easy way to know what is happening in other sessions. Sometimes you would like to know what has happened since your last read, without reading everything again, e.g. when you need to pass a large dataset to a Control Add-in.</p>
|
||||
<p>A common way of handling this with Ledger Tables is to make note of the last record you read, and continuously poll to see if there are any new records. However this is restricted to strictly sequentially entered tables.</p>
|
||||
<h3 id=solution>Solution</h3>
|
||||
<h2 id=solution>Solution</h2>
|
||||
<p>There is a common pattern in many other languages called <a href=https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern>Publish-Subscribe</a> (or PubSub) that solves the same issue. We can implement the same pattern in NAV using a Table as a message queue platform and polling this table. We have named this pattern “Cross Session Events” in order to avoid confusion with the standard NAV Events which use the terms Publisher and Subscriber, and to try and describe more accurately when you would need this pattern.</p>
|
||||
<p>The pattern has four components:</p>
|
||||
<ul>
|
||||
|
|
@ -237,100 +231,104 @@ BC AL Help . dev
|
|||
<li><strong>Message Broker</strong>: This distributes all messages sent in to all Subscribers that have expressed an interest (i.e. the message is within their filters).</li>
|
||||
<li><strong>Message Queue</strong>: To hold the messages for each Subscriber. Generally once these messages are read, they are deleted.</li>
|
||||
</ul>
|
||||
<h3 id=example>Example</h3>
|
||||
<h2 id=example>Example</h2>
|
||||
<p>An example of this would be when we have multiple users looking at the same set of data and we want their screens to update in “real time” whenever one of them makes a change, without doing a full refresh. We will use the <a href=/nav/w/designpatterns/248.observer>Observer pattern</a> to capture the change (act as the Publisher) and then create a Table to hold Subscribers and Filters (Change Observer), a Table to be the Message Queue (Change Notification), and a Codeunit to be the Message Broker and help with the polling (ObserverMgt).</p>
|
||||
<p>Below are the table definitions:</p>
|
||||
<pre><code>**Change Observer:
|
||||
**"Table ID" Integer "Observable Table"
|
||||
"Server ID" Integer
|
||||
"Session ID" Integer
|
||||
|
||||
**Change Notification:
|
||||
**"Table ID" Integer "Observable Table"
|
||||
"Server ID" Integer
|
||||
"Session ID" Integer
|
||||
"Entry No." Integer AutoIncrement
|
||||
"Type of Change" Option Insert,Modify,Delete,Rename
|
||||
"Record ID" RecordID
|
||||
... (other fields to indicate what has changed)
|
||||
</code></pre>
|
||||
<p>The Change Observer table identifies the Subscriber using Server ID and Session ID, and then in this example there is only one filter, which is the Table ID we want to listen to any changes. In this case all three fields are in the Primary Key.</p>
|
||||
<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-al data-lang=al> <span style=color:#75715e>//Change Observer:
|
||||
</span><span style=color:#75715e></span> <span style=color:#75715e>//"Table ID" Integer "Observable Table"
|
||||
</span><span style=color:#75715e></span> "Server ID" <span style=color:#66d9ef>Integer</span>
|
||||
"Session ID" <span style=color:#66d9ef>Integer</span>
|
||||
|
||||
<span style=color:#75715e>//Change Notification:
|
||||
</span><span style=color:#75715e></span> <span style=color:#75715e>//"Table ID" Integer "Observable Table"
|
||||
</span><span style=color:#75715e></span> "Server ID" <span style=color:#66d9ef>Integer</span>
|
||||
"Session ID" <span style=color:#66d9ef>Integer</span>
|
||||
"Entry No." <span style=color:#66d9ef>Integer</span> AutoIncrement
|
||||
"Type of Change" <span style=color:#66d9ef>Option</span> Insert,<span style=color:#66d9ef>Modify</span>,Delete,Rename
|
||||
"Record ID" <span style=color:#66d9ef>RecordID</span>
|
||||
<span style=color:#f92672>... (</span>other <span style=color:#66d9ef>fields</span> <span style=color:#66d9ef>to</span> indicate what has changed<span style=color:#f92672>)
|
||||
</span></code></pre></div><p>The Change Observer table identifies the Subscriber using Server ID and Session ID, and then in this example there is only one filter, which is the Table ID we want to listen to any changes. In this case all three fields are in the Primary Key.</p>
|
||||
<p>The Change Notification table then has the same three fields plus an Entry No. as its Primary Key, and in this example borrows heavily from the Change Log code to fill in the rest of the message.</p>
|
||||
<p>_<strong>Note:</strong> _Other examples of the pattern could have very different fields to identify the Subscriber, Filters and then whatever fields needed for content of the Message.</p>
|
||||
<p>Our Message Broker Codeunit will also serve as a central place to create Subscribers (Listen and StopListening functions) and a place to Poll for Messages. Note that the Poll function deletes the Messages as it reads them.</p>
|
||||
<pre><code>**Listen(TableID : Integer)**
|
||||
WITH Observer DO BEGIN
|
||||
"Table ID" := TableID;
|
||||
"Server ID" := SERVICEINSTANCEID;
|
||||
"Session ID" := SESSIONID;
|
||||
INSERT(TRUE);
|
||||
COMMIT;
|
||||
END;
|
||||
**StopListening(TableID : Integer)
|
||||
**WITH Observer DO BEGIN
|
||||
RESET;
|
||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||
SETRANGE("Session ID",SESSIONID);
|
||||
SETRANGE("Table ID",TableID);
|
||||
DELETEALL(TRUE);
|
||||
COMMIT;
|
||||
END;
|
||||
**NotifyAll(ChangeNotification : Record "Change Notification")**
|
||||
WITH Observer DO BEGIN
|
||||
RESET;
|
||||
SETRANGE("Table ID",ChangeNotification."Table ID");
|
||||
IF FINDSET THEN REPEAT
|
||||
Notify(Observer,ChangeNotification);
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
**Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification")**
|
||||
WITH ChangeNotification DO BEGIN
|
||||
"Server ID" := Observer."Server ID";
|
||||
"Session ID" := Observer."Session ID";
|
||||
"Entry No." := 0;
|
||||
INSERT;
|
||||
END;
|
||||
**Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification")**
|
||||
WITH ChangeNotification DO BEGIN
|
||||
TempChangeNotification.RESET;
|
||||
TempChangeNotification.DELETEALL;
|
||||
RESET;
|
||||
SETRANGE("Table ID",TableID);
|
||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||
SETRANGE("Session ID",SESSIONID);
|
||||
IF FINDSET THEN REPEAT
|
||||
TempChangeNotification := ChangeNotification;
|
||||
TempChangeNotification.INSERT;
|
||||
MARK(TRUE);
|
||||
UNTIL NEXT = 0;
|
||||
MARKEDONLY(TRUE);
|
||||
DELETEALL;
|
||||
END;
|
||||
</code></pre>
|
||||
<p>**__**The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page:</p>
|
||||
<pre><code>**OnQueryClosePage(CloseAction : Action None) : Boolean**
|
||||
ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking");
|
||||
**Timer::AddInReady()**
|
||||
IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN
|
||||
CurrPage.Timer.Ping(1000);
|
||||
**Timer::Pong()**
|
||||
CallUpdate;
|
||||
CurrPage.Timer.Ping(1000);
|
||||
**LOCAL CallUpdate()**
|
||||
ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification);
|
||||
WITH TempChangeNotification DO BEGIN
|
||||
IF FINDSET THEN REPEAT
|
||||
IF "Type of Change" = "Type of Change"::Delete THEN BEGIN
|
||||
...
|
||||
END ELSE IF RecRef.GET("Record ID") THEN BEGIN
|
||||
...
|
||||
END;
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
</code></pre>
|
||||
<h3 id=consequences>Consequences</h3>
|
||||
<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-al data-lang=al> <span style=color:#75715e>//Listen(TableID : Integer)**
|
||||
</span><span style=color:#75715e></span> <span style=color:#66d9ef>WITH</span> Observer <span style=color:#66d9ef>DO</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
"Table ID" <span style=color:#f92672>:=</span> TableID;
|
||||
"Server ID" <span style=color:#f92672>:=</span> SERVICEINSTANCEID;
|
||||
"Session ID" <span style=color:#f92672>:=</span> SESSIONID;
|
||||
INSERT<span style=color:#f92672>(</span>TRUE<span style=color:#f92672>)</span>;
|
||||
COMMIT;
|
||||
<span style=color:#66d9ef>END</span>;
|
||||
|
||||
<span style=color:#75715e>//StopListening(TableID : Integer)
|
||||
</span><span style=color:#75715e></span> <span style=color:#66d9ef>WITH</span> Observer <span style=color:#66d9ef>DO</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
RESET;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Server ID",SERVICEINSTANCEID<span style=color:#f92672>)</span>;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Session ID",SESSIONID<span style=color:#f92672>)</span>;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Table ID",TableID<span style=color:#f92672>)</span>;
|
||||
DELETEALL<span style=color:#f92672>(</span>TRUE<span style=color:#f92672>)</span>;
|
||||
COMMIT;
|
||||
<span style=color:#66d9ef>END</span>;
|
||||
|
||||
<span style=color:#75715e>//NotifyAll(ChangeNotification : Record "Change Notification")**
|
||||
</span><span style=color:#75715e></span> <span style=color:#66d9ef>WITH</span> Observer <span style=color:#66d9ef>DO</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
RESET;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Table ID",ChangeNotification<span style=color:#f92672>.</span>"Table ID"<span style=color:#f92672>)</span>;
|
||||
<span style=color:#66d9ef>IF</span> FINDSET <span style=color:#66d9ef>THEN</span> <span style=color:#66d9ef>REPEAT</span>
|
||||
Notify<span style=color:#f92672>(</span>Observer,ChangeNotification<span style=color:#f92672>)</span>;
|
||||
<span style=color:#66d9ef>UNTIL</span> NEXT = <span style=color:#ae81ff>0</span>;
|
||||
<span style=color:#66d9ef>END</span>;
|
||||
|
||||
<span style=color:#75715e>//Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification")**
|
||||
</span><span style=color:#75715e></span> <span style=color:#66d9ef>WITH</span> ChangeNotification <span style=color:#66d9ef>DO</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
"Server ID" <span style=color:#f92672>:=</span> Observer<span style=color:#f92672>.</span>"Server ID";
|
||||
"Session ID" <span style=color:#f92672>:=</span> Observer<span style=color:#f92672>.</span>"Session ID";
|
||||
"Entry No." <span style=color:#f92672>:=</span> <span style=color:#ae81ff>0</span>;
|
||||
INSERT;
|
||||
<span style=color:#66d9ef>END</span>;
|
||||
|
||||
<span style=color:#75715e>//Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification")**
|
||||
</span><span style=color:#75715e></span> <span style=color:#66d9ef>WITH</span> ChangeNotification <span style=color:#66d9ef>DO</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
TempChangeNotification<span style=color:#f92672>.</span>RESET;
|
||||
TempChangeNotification<span style=color:#f92672>.</span>DELETEALL;
|
||||
RESET;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Table ID",TableID<span style=color:#f92672>)</span>;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Server ID",SERVICEINSTANCEID<span style=color:#f92672>)</span>;
|
||||
SETRANGE<span style=color:#f92672>(</span>"Session ID",SESSIONID<span style=color:#f92672>)</span>;
|
||||
<span style=color:#66d9ef>IF</span> FINDSET <span style=color:#66d9ef>THEN</span> <span style=color:#66d9ef>REPEAT</span>
|
||||
TempChangeNotification <span style=color:#f92672>:=</span> ChangeNotification;
|
||||
TempChangeNotification<span style=color:#f92672>.</span>INSERT;
|
||||
MARK<span style=color:#f92672>(</span>TRUE<span style=color:#f92672>)</span>;
|
||||
<span style=color:#66d9ef>UNTIL</span> NEXT = <span style=color:#ae81ff>0</span>;
|
||||
MARKEDONLY<span style=color:#f92672>(</span>TRUE<span style=color:#f92672>)</span>;
|
||||
DELETEALL;
|
||||
<span style=color:#66d9ef>END</span>;
|
||||
</code></pre></div><p>**__**The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page:</p>
|
||||
<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-al data-lang=al> <span style=color:#75715e>//OnQueryClosePage(CloseAction : Action None) : Boolean**
|
||||
</span><span style=color:#75715e></span> ObserverMgt<span style=color:#f92672>.</span>StopListening<span style=color:#f92672>(</span><span style=color:#66d9ef>DATABASE</span>::"NAV Whiteboard Booking"<span style=color:#f92672>)</span>;
|
||||
|
||||
<span style=color:#75715e>//Timer::AddInReady()**
|
||||
</span><span style=color:#75715e></span> <span style=color:#66d9ef>IF</span> ObserverMgt<span style=color:#f92672>.</span>Listen<span style=color:#f92672>(</span><span style=color:#66d9ef>DATABASE</span>::"NAV Whiteboard Booking"<span style=color:#f92672>) </span><span style=color:#66d9ef>THEN</span>
|
||||
CurrPage<span style=color:#f92672>.</span>Timer<span style=color:#f92672>.</span>Ping<span style=color:#f92672>(</span><span style=color:#ae81ff>1000</span><span style=color:#f92672>)</span>;
|
||||
|
||||
<span style=color:#75715e>//Timer::Pong()**
|
||||
</span><span style=color:#75715e></span> CallUpdate;
|
||||
CurrPage<span style=color:#f92672>.</span>Timer<span style=color:#f92672>.</span>Ping<span style=color:#f92672>(</span><span style=color:#ae81ff>1000</span><span style=color:#f92672>)</span>;
|
||||
|
||||
<span style=color:#75715e>//LOCAL CallUpdate()**
|
||||
</span><span style=color:#75715e></span> ObserverMgt<span style=color:#f92672>.</span>Poll<span style=color:#f92672>(</span><span style=color:#66d9ef>DATABASE</span>::"NAV Whiteboard Booking",TempChangeNotification<span style=color:#f92672>)</span>;
|
||||
<span style=color:#66d9ef>WITH</span> TempChangeNotification <span style=color:#66d9ef>DO</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
<span style=color:#66d9ef>IF</span> FINDSET <span style=color:#66d9ef>THEN</span> <span style=color:#66d9ef>REPEAT</span>
|
||||
<span style=color:#66d9ef>IF</span> "Type of Change" = "Type of Change"::Delete <span style=color:#66d9ef>THEN</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
<span style=color:#f92672>...
|
||||
</span><span style=color:#f92672> </span><span style=color:#66d9ef>END</span> <span style=color:#66d9ef>ELSE</span> <span style=color:#66d9ef>IF</span> RecRef<span style=color:#f92672>.</span>GET<span style=color:#f92672>(</span>"Record ID"<span style=color:#f92672>) </span><span style=color:#66d9ef>THEN</span> <span style=color:#66d9ef>BEGIN</span>
|
||||
<span style=color:#f92672>...
|
||||
</span><span style=color:#f92672> </span><span style=color:#66d9ef>END</span>;
|
||||
<span style=color:#66d9ef>UNTIL</span> NEXT = <span style=color:#ae81ff>0</span>;
|
||||
<span style=color:#66d9ef>END</span>;
|
||||
</code></pre></div><h2 id=consequences>Consequences</h2>
|
||||
<p>The PingPong control is only available on the Windows Client, so if you want to use another client you will need to use another solution to Poll for Messages. Therefore this pattern is not always going to be “real time”.</p>
|
||||
<h3 id=related-topics>Related Topics</h3>
|
||||
<h2 id=related-topics>Related Topics</h2>
|
||||
<p>This pattern was originally described in the following blog:</p>
|
||||
<p><a href=https://geeknikolai.wordpress.com/2015/10/30/pubsub-pattern-in-dynamics-nav-2016/>https://geeknikolai.wordpress.com/2015/10/30/pubsub-pattern-in-dynamics-nav-2016/</a></p>
|
||||
<p>Below is the Wikipedia link to the PubSub pattern</p>
|
||||
|
|
@ -347,12 +345,12 @@ END;
|
|||
<div style=left:-1000px;overflow:scroll;position:absolute;top:-1000px;border:none;box-sizing:content-box;height:200px;margin:0;padding:0;width:200px>
|
||||
<div style=border:none;box-sizing:content-box;height:200px;margin:0;padding:0;width:200px></div>
|
||||
</div>
|
||||
<script src=/js/clipboard.min.js?1636104707></script>
|
||||
<script src=/js/perfect-scrollbar.min.js?1636104707></script>
|
||||
<script src=/js/perfect-scrollbar.jquery.min.js?1636104707></script>
|
||||
<script src=/js/jquery.svg.pan.zoom.js?1636104707></script>
|
||||
<script src=/js/featherlight.min.js?1636104707></script>
|
||||
<script src=/js/modernizr.custom-3.6.0.js?1636104707></script>
|
||||
<script src=/js/relearn.js?1636104707></script>
|
||||
<script src=/js/clipboard.min.js?1636386789></script>
|
||||
<script src=/js/perfect-scrollbar.min.js?1636386789></script>
|
||||
<script src=/js/perfect-scrollbar.jquery.min.js?1636386789></script>
|
||||
<script src=/js/jquery.svg.pan.zoom.js?1636386789></script>
|
||||
<script src=/js/featherlight.min.js?1636386789></script>
|
||||
<script src=/js/modernizr.custom-3.6.0.js?1636386789></script>
|
||||
<script src=/js/relearn.js?1636386789></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue