Multi-Tutorial zu WebRequest, Threading, EBC-Architektur und einigen fortgeschrittenen Features von VB 2010

    • VB.NET

    Es gibt 9 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Multi-Tutorial zu WebRequest, Threading, EBC-Architektur und einigen fortgeschrittenen Features von VB 2010

      Hallo allerseits!

      Der übernächste Post ist der erste Teil eines 3-teiligen Tutorials, in dem ich eine kleine Anwendung besprechen werde.
      Die App setzt eine Such-Anfrage an eine WebSite ab, extrahiert dem geantworteten HTML die Treffer, ruft dann die Treffer-Urls auf, und extrahiert aus deren Antwort-Html wiederum ein paar Detail-Informationen, zeigt sie an und speichert sie ab.


      Die Anwendung erfordert das Zusammenspiel verschiedener Technologien (WebRequest, Threading, Delegaten, Databinding) und nutzt einige sehr interessante Features von VB (Extension-Methods, anonyme Methoden, Linq)

      Jedes des Vorgenannten wäre eines eigenen Tuts würdig, und dann hätte man immer noch kein Beispiel dafür, wies aussehen kann, wenn alles dieses wie ein Räderwerk ineinandergreift und zusammenarbeitet.
      Deshalb habich dieses Tutorial als Multi-Tut konzipiert, und will einfach alles besprechen, was auftritt.

      Das konzeptionell besonders Besondere an diesem Tut ist, dassichs nicht als fertig in die Tutorials einstelle, sondern zunächst hier poste, und ihr könnt Fragen dazu stellen.
      Davon verspreche ich mir einerseits ein gesteigertes Interesse, vor allem aber hoffe ich durch den Dialog die Verständlichkeit meiner Erläuterungen erhöhen zu können ;).
      Und ausserdem besser auf das zu fokussieren, was den Leser interessiert.
      Obwohls schon ziemlich umfangreich ist, ist das Tutorial also u.U. total unfertig und kann sich noch sehr ändern, indem durch die Diskussionen sich Schwerpunkte umgewichten oder auch ganz neue Schwerpunkte hinzukommen.

      Also ich füge die App schomal an, und ihr könnt damit herumspielen. Um Bugreports bitte ich per PN, damit in diesem Thread wirklich v.a. Verständnisfragen geklärt werden.

      Jo, dann bin ich ja mal gespannt, wie rege die Beteiligung ausfallen wird. Im schlimmsten Fall hat halt niemand eine Frage dazu - dann isses also doch schon fertig wies ist, und landet dann halt so in den Tutorials. :)
      Dateien
      • CineFactsUp02.zip

        (104,04 kB, 298 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Naja - ich bin guter Dinge, das allerlei programmierspezifische Fragen noch kommen werden - zB du fragst soeben nach Threading.
      Hoffentlich sind die Mods also gnädig mit meinem Formfehler, dass nicht der TE die erste Frage stellt, bzw. keine eiglich programmierspezifische (ich bitte ja um eure Mithilfe). ;)
      Jdfs. zum Threading: Vorgestellt wird Threading nach dem asynchronen Entwurfsmuster, und zwar in 2 Varianten:
      1. Threading um zu vermeiden, dass der Gui-Thread durch einen lang andauernden Vorgang blockiert wird, und die Anwendung einfriert
      2. Threading für Performance-Gewinn, um mehrere langsame Vorgänge parallel ablaufen zu lassen anstatt hintereinander. Dieses sog. Fork-Join-Threading ist nur dann angesagt, wenn die langsame Geschwindigkeit nicht von der Taktrate abhängt, sondern von anderen Resourcen (hier: Internet-Zugriff).
        Bei einer 100%igen CPU-Auslastung bringt Parallelisierung keinen Gewinn, denn dabei bekommt jeder Thread logischerweise nur einen Anteil der CPU-Power, und ob man nun einen großen Vorgang mit 100% CPU hat oder 10 parallel laufende kleine Vorgänge - jeder nur mit 10% - macht kein Unterschied.
      So, jetzt aber - 1.Teil:

      Tutorial um eine KinoFilm-Suchmaschine mit Internet-Zugriff

      Die Anwendung
      soll eine Suche auf der Site http://www.cinefacts.de/ automatisieren. Nachgebildet wird ungefähr die folgende Nutzung der WebSite durch einen Film-Interessierten User:
      1. Der User gibt einen Suchbegriff ein, zB. 'Bullets', und startet die Suche
      2. Daraufhin bekommt er folgendes QueryResult, in dem verschiedene Treffer (Matches) aufgeführt sind:


      3. Es interessieren ihn die Detail-Informationen der Such-Ergebnisse, auf die durch die Detail-Buttons verlinkt ist - hier das Beispiel-Detail von 22 Bullets:


        Insbesondere auf den KurzInhalt hatters abgesehen - runter-gescrollt:


      Obiger Vorgang wird also automatisiert: Die Anwendung nimmt vom User den Pattern 'bullets' entgegen, bildet daraus die url (s. Screenshot):
      http://www.cinefacts.de/suche/suche.php?name=bullets&go.x=0&go.y=0
      und ruft per WebRequest das Html ab - ohne den Overhead eines WebBrowsers.
      Aus dem geantworteten Html extrahiert sie die zu den 'Detail'-Buttons gehörigen urls, und ruft die auch auf.
      Aus deren Ergebnis-Html werden Film-Titel und KurzInhalt extrahiert.
      Query und gefundene FilmInformationen werden in einem Dataset gespeichert, und als ParentChildView präsentiert:

      Der User kann dann aus seinen Queries auswählen, und die Film-Infos einsehen: Die gefundenen FilmTitel tabellarisch, und den KurzInhalt in einer Extra-Textbox, die immer den KurzInhalt des angewählten FilmTitels anzeigt.

      Technologien
      Bitte beachte die Links: die führen auf MSDN-Artikel, die ausführlicher erläutern, bzw. auch fundierter, als ich es kann - die Links sind gewissermaßen Bestandteil des Tutorials:
      • WebRequest: eine Framework-Klasse, mit der urls aus dem Internet angefordert werden können (kann natürlich noch wesentlich mehr, als was in der App genutzt wird)
      • Threading mit IAsyncResult: Da die url-Anforderung einige Zeit in Anspruch nimmt, muß sie in einem eigenen Thread erfolgen, weil ansonsten die Anwendung 'einfrieren' würde, bis die angeforderten Daten bereitstehen. Außerdem ist erheblich optimiert dadurch, dass die einzelnen Detail-Informationen ebenfalls jeweils im eigenen Thread abgerufen werden. Im obigen Beispiel gehen 5 Detail-Requests gleichzeitig raus (und werden beantwortet), anstatt dass eine nach der anderen abgesetzt würde.
      • Delegaten: Sowohl Threading als auch die gewählte Architektur (ich komme noch dazu) erfordern einen fortgeschrittenen Umgang mit Delegaten. Ich treibe allerlei Kapriolen damit, insbesondere mit...
      • generischen Delegaten: Das sind Action(Of T), Action(Of T1, T2), Func(Of T), Func(Of T1, T2),... Mit diesen Delegaten kann man praktisch jede Methoden-Signatur bilden, die man braucht.
      • Extension-Methods: eine fabelhafte Erfindung, mit der man Public Methods schreiben kann, die aufgerufen werden wie Objekt-Methoden. Damit kann man beliebige Klassen quasi erweitern, ohne sie beerben zu müssen, natürlich auch generische Klassen, und selbst Interfaces.
      • anonyme Methoden: Anstatt einer Delegat-Variable per AddressOf <MethodName> eine Delegat-Instanz zuzuweisen kann man die Methode auch anonym hinschreiben und direkt zuweisen. Das macht Code u.U. viel kompakter. Man kann sogar innerhalb von Methoden anonyme Methoden schreiben und aufrufen - optimale Kapselung! (Leider fund ich auf MSDN nur einen c#-Artikel hierzu. Der hat natürlich volle Gültigkeit auch für VB, ist aber c# - wenn jmd. was besseres findet - immer her damit!)
      • anonymer DatenTyp: meist ein unnützes Gimmick, weil mans nicht als Argument an Methoden weiterreichen kann - aber an einer Stelle habe ich doch Verwendung dafür ;)
      • Regex: wird natürlich exzessiv eingesetzt, um aus Html-Code Informationen zu extrahieren. Auch Einsatz der so unbekannten wie mächtigen MatchEvaluator-Technik zum TextErsatz. Obwohl die App ohne Regex unmöglich wäre, verweise ich einfach auf das Regex-Tut von link_275 sowie auf meinen RegexTester, mit dem man sich sicherlich bei weitem besser mit Regex vertraut machen kann, als wenn ich hier jetzt alles, wassich weiß zum Thema, runterlabern würde. (Wirklich geläufig ist mir die Regex-Syntax gar nicht - selbst für (scheinbar) einfache Ausdrücke benötige ich den Regextester)
      • Linq
      • typisiertes Dataset und designergestütztes Databinding: Das Verhalten des Views, welches zu einem angewählten Datensatz automatisch die Details anzeigt, ist komplett durch Festlegungen der Bindings im Designer definiert - dafür wurde keine Zeile Code geschrieben. Da ich auf MSDN kein vernünftiges Beispiel zum Erstellen eines typisierten Datasets gefunden habe (nur ein überaus unvernünftiges), verweise ich auf mein mein eigenes Tutorial "DatasetOnly" auf Movie-Tuts


      Architektur1: Solution mit 2 Projekten
      Das HauptProjekt verwendet viele kleine und große Funktionen, die auch in vielen anderen Anwendungen Verwendung finden. Diese "Helpers" sind in einer Helpers-Dll ausgelagert.

      Architektur2: EBC
      Das ist jetzt ein sehr großes Thema. EBC steht für "Event-based-components". Mir ist das Konzept bekannt geworden durch diese Site. Viel dazu herumdiskutiert habe ich hier. Und hier stelle ich ein anderes (ich finde gelungenes) Experiment mit dieser Architektur vor.

      Die Architektur dieser Anwendung im EBC-Diagramm:

      EBC betrachtet eine Anwendung als Komposition von mehrheitlich gänzlich unabhängigen Komponenten. Eine Komponente ruft niemals die Methode einer anderen Komponente auf, sondern sie versendet einfach ein Event, und weiß nicht, wer es verarbeitet. Andererseits bietet sie Public Methoden an, und weiß nicht, von wo die aufgerufen werden. Die Events werden als Out-Pins bezeichnet, und die Methoden als In-Pins.
      Eine übergeordnete Komponente verknüpft nun den Out-Pin der einen Komponente mit dem In-Pin einer anderen. Im Diagramm sind diese Verknüpfungen als Pfeile dargestellt, und damit ist der KommunikationsFluss der Anwendung vollständig abgebildet.
      Man unterscheidet bei den Komponenten zwischen "Atomen", die die eigentliche Arbeit machen, und "Platinen", deren einzige Aufgabe ist, untergeordnete Komponenten (Atome oder ebenfalls Platinen) miteinander zu verknüpfen.
      Dieses Architektur-Prinzip kann beliebig komplex werden, ohne an Systematik zu verlieren. Man kann sich ins Verständnis jeder Anwendung einarbeiten, indem man zunächst die Haupt-Platine betrachtet, und (wenn vorhanden) auch die EBC-Diagramme der UnterPlatinen. Jede Komponente hat eine möglichst einfach definierte Aufgabe, und wenn alle Platinen verstanden sind, kennt man die Anwendung.

      Jetzt aber zur Funktionsweise obigen EbcDiagramms (bitte nochmal angucken):
      Also das FrontEnd ist das Form - da kann der User den Pattern eingeben (legt damit einen Query-Datensatz an), und auf "Search" klicksen. Daraufhin disabled das Form seine Steuerelemente (damit der User nicht herumklicksen kann, während die Query läuft) und sendet dann den Pattern an den QuerySearcher. Der bastelt daraus eine url und ruft die ab. Das Ergebnis (Html-Code) sendet er an den MatchSearcher. Der extrahiert daraus die Detail-urls, und ruft die ab. Jedes Detail-url-Ergebnis sendet er an den MatchAnalizer, der die Detail-Informationen aus dem Html extrahiert, und daraus eine MatchRow bastelt, die er der MatchDataTable des Datasets zufügt. Um eine MatchRow zu adden benötigt der Analizer die QueryRow, zu der der Match gehört. Die holt er sich vom FrontEnd, von dem die Suche ja ausging.
      Dieses Holen ist interessant: Der KommunikationsFluss geht vom Analizer zum FrontEnd, aber der DatenFluss ist gegenläufig: vom FrontEnd zum Analizer. Es ist nicht ein Daten verschickendes Event, sondern ein Daten holendes. (Tatsächlich ist es gar kein Event im Sinne von VB, sondern es ist ein Func(Of QueryRow) - Delegat, also ein Delegat, der eine Methode enthalten kann, die eine QueryRow zurückgibt.)
      Wenn alle Detail-url-Ergebnisse im MatchSearcher eingetroffen und verarbeitet sind, schickt der MatchSearcher ein leeres Event QueryDone ans FrontEnd, damit dieses weiß, dasses seine Steuerelemente wieder enablen kann.

      Noch ein Bild der beiden Tabellen des Datasets, damit klar ist, warum ich von 'QueryRow' und 'MatchRow' rede:


      Noch paar Bemerkungen zu EBC: Im Ebc-Diagramm ist nicht jede Eigenheit einer Verknüpfung dargestellt. ZB nicht eindeutig ist, ob eine Komponente auf einen InPin-Input mit nur einmaligem OutPin-Output antwortet, ob sie den Output mehrfach betätigt (oder u.U. auch gar nicht), und ob zwischen Input und Output ein ThreadWechsel stattfindet. Und bei mehreren OutPins ist auch die Reihenfolge des Outputs nicht eindeutig.
      Aber ich denke, in diesem einfachen Beispiel versteht man das aus dem Zusammenhang.
      Auch ist EBC eine noch sehr junge "Wissenschaft", und die Semantik von EBC-Diagrammen ist noch gar nirgends verbindlich festgelegt. Nicht einmal die Art der Pins ist vorgeschrieben (zumindest halte ich mich nicht dran ;)). Ich weiche zB. ganz unbekümmert erheblich von den Vorgaben Ralph WestPhals ab, etwa wenn ich den GetQueryRow-Pin als Func(Of QueryRow) implementiere - Ralph würde hier eine Action(Of Action(Of QueryRow)) verwenden (das ist konzeptionell reiner, auch mächtiger, aber bisserl umständlicher und schwerer zu verstehen).
      Auch finde ich die explizite Implementation eines Interfaces für Pins überflüssig: Ein Pin ist ein Delegat mit eindeutiger Signatur - was dranpasst, passt, alles andere eben nicht - wozu also ein zusätzliches Interface noch mit-pflegen?
      Wesentlich ist die vollkommene Entkopplung gleichrangiger Komponenten, und da ich Ralph als durchaus undogmatischen Denker kennengelernt habe, denke ich, er wird mir vergeben ;)

      Was ich am Ebc-Diagramm besonders fabelhaft finde ist, dasses (anders als etwa UML) tatsächlich eine Konzeptions-Hilfe darstellt. Ich habe die App tatsächlich zunächst im Diagramm konzipiert, und dann die Klassen aufgesetzt und die Pins reingeschrieben, deren Name und Signatur ich dem Diagramm entnahm:
      • QueryRequest: Action(Of String)
      • MatchSearchRequest: Action(Of String)
      • AnalizeRequest: Action(Of String)
      • GetQueryRow: Func(Of QueryRow)
      • QueryDone: Action
      Danach konnte ich eine Klasse nach der anderen ausprogrammieren und fertig. (Tatsächlich zeigte sich bei der Entwicklung des MatchSearchers die Notwendigkeit, die Analyse der Detail-url-Responses in eine weitere Komponente auszulagern, aber das Diagramm war in 5 min korrigiert)

      So, damit hätten wir EBC und die (einzige) Platine. Jetzt gehts weiter mitm...

      Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Code
      Ich bespreche hauptsächlich die Haupt-Anwendung - aufs Helpers-Project gehe ich erstmal nur auszugsweise ein. Bei der Code-Besprechung erweist sich die vollständige Unabhängigkeit der Atome untereinander als sehr hilfreich fürs Verständnis.
      Denn die Klassen sind sehr klein, haben eine klare Aufgabe, und alles zur Erledigung erforderliche ist innerhalb der Klasse enthalten - auf einen Blick.

      QuerySearcher

      VB.NET-Quellcode

      1. Public Class QuerySearcher
      2. Public RequestMatches As Action(Of String)
      3. Private _WrapPattern As String = "http://www.cinefacts.de/suche/suche.php?name=####&go.x=0&go.y=0"
      4. ''' <summary>
      5. ''' setzt einen Pattern in eine url ein, und fährt damit asynchron einen Webrequest ab.
      6. ''' Der AntwortString wird per RequestMatches-Delegat abgeschickt
      7. ''' </summary>
      8. Public ReadOnly RequestQuery As Action(Of String) = _
      9. Sub(pattern As String)
      10. Dim wr = WebRequest.Create(_WrapPattern.Replace("####", pattern))
      11. Dim callback As AsyncCallback = Sub(ar) RequestMatches.Invoke(wr.DownloadString(Encoding.Default, ar))
      12. wr.BeginGetResponse(callback, Nothing)
      13. End Sub
      14. End Class
      (Das mag mit der schnuckligste Code sein, den ich je produziert habe ;))

      Es ist nichtmal eine einzige richtige Methode in der Klasse, sondern nur eine readonly Public Variable RequestQuery vom Typ Action(Of String) (ein Delegat).
      Dieser Delegat wird gleich mit einer anonymen Methode initialisiert. Die lokale Variable callback ist ebenfalls ein Delegat mit anonymer Methode, also eine anonyme Methode innerhalb einer anonymen Methode.
      callback muß die Signatur AsyncCallback haben, d.h.: Sub(ar As IAsyncResult) - sonst habich für wr.BeginGetResponse() kein Argument.
      Wir sehen hier Threading nach dem asynchronen Entwurfsmuster[
      Beim asynchronen Entwurfsmuster hat man eine Begin-Funktion und eine End-Funktion (hier: BeginGetResponse() und EndGetResponse()). BeginGetResponse startet den Vorgang, und bekommt einen callback mit, der zurückgerufen wird, wenn das Ergebnis bereit steht. Das Ergebnis kann dann mit EndGetResponse(ar) abgerufen werden.
      EndGetResponse braucht das IAsyncResult ar, welches beim Rückruf freundlicherweise mitgeschickt wird.
      Das Abrufen von EndGetResponse(ar) mache ich in diesem Fall aber nicht hier, sondern in der WebRequest.DownloadString-Extension, die ja innerhalb des hiesigen callback-Codes aufgerufen wird.
      Der callback schickt den DownloadString gleich weiter, per RequestMatches.Invoke()

      Irgendwie ziemlich krude, das asynchrone Entwurfsmuster. Aber ich bin da nicht dran schuld. Ich habe hier nur einen Weg gefunden, MultiThreading nach dem asynchronen Entwurfsmuster in 2 Zeilen hinter mich zu bringen ;)

      Die Extension WebRequest.DownloadString bedarf der Erläuterung:

      VB.NET-Quellcode

      1. Public Module Extensions
      2. ''' <summary>
      3. ''' lädt den Antwort-Stream des WebRequests in einen String und returnt diesen.
      4. ''' Unterstützt asynchronen Download, wenn das optionale asyncResult angegeben wird
      5. ''' </summary>
      6. <Extension()> _
      7. Public Function DownloadString(ByVal Subj As WebRequest, _
      8. ByVal encoding As Encoding, Optional ByVal asyncResult As IAsyncResult = Nothing) As String
      9. Using resp = If(asyncResult.NotNull, Subj.EndGetResponse(asyncResult), Subj.GetResponse()), _
      10. rd = New StreamReader(resp.GetResponseStream, encoding)
      11. Return rd.ReadToEnd
      12. End Using
      13. End Function
      14. End Module
      Ebenfalls unerhört schnucklig, findich ;)
      Sind die verwendeten Technologien (WebRequest, StreamReader, Extension, asynchrones Entwurfsmuster, Using-Block) bekannt, bedarfes tatsächlich keines weiteren Kommentars.
      Beachte, dass der Using-Block gleich 2 Variablen instanziert (und damit beim Verlassen aufräumt): die WebResponse und den StreamReader.

      MatchSearcher

      VB.NET-Quellcode

      1. Public Class MatchSearcher
      2. Public QueryDone As Action
      3. Public AnalizeRequest As Action(Of String)
      4. Private _LinkPrefix As String = "http://www.cinefacts.de"
      5. Private _rgxLink As New Regex("\<a href=""(/kino/.*?/filmdetails.html)""\>Details\</a\>")
      6. ''' <summary>
      7. ''' sucht aus dem Html-Quelltext die urls heraus, die auf Sites mit DetailInformationen führen.
      8. ''' Die DetailInformationen werden dann parallel abgerufen.
      9. ''' </summary>
      10. Public ReadOnly RequestMatches As Action(Of String) = _
      11. Sub(sHtml As String)
      12. Dim urls = From mt In _rgxLink.Matches(sHtml).Cast(Of Match)() _
      13. Select _LinkPrefix & mt.Groups(1).Value Distinct
      14. Dim requestInfos = From url In urls Select New With { _
      15. .wr = WebRequest.Create(url), _
      16. .asyncResult = .wr.BeginGetResponse(Nothing, Nothing)}
      17. For Each inf In requestInfos.ToArray
      18. AnalizeRequest.Invoke(inf.wr.DownloadString(Encoding.Default, inf.asyncResult))
      19. Next
      20. QueryDone.Invoke()
      21. End Sub
      22. End Class
      Wieder ein Public Readonly Action(Of String)-Delegat. Die anonyme Methode treibt folgendes:
      Zunächst wird mittels LINQ aus den Treffern des Regex je die lokale url (mt.Groups(1)) selektiert, der der _LinkPrefix vorangestellt wird, damit die url global addressiert. Das LINQ-Schlüsselwort DISTINCT sortiert evtl. doppelt vorkommende urls gleich aus.
      Dann werden requestInfos gesammelt - das ist eine anonyme Klasse mit den Properties .wr As WebRequest und .asyncResult As IAsyncResult.
      Hier wird der .BeginGetResponse()-Funktion kein callback mitgegeben, wie beim QuerySearcher - stattdessen wird das asyncResult gespeichert.
      Dann werden alle requestInfos durchlaufen, und der DownloadString an den Analizer geschickt.
      Beachte das "Fork-Join-Threading": .wr.BeginGetResponse() rast in Nullzeit durch, denn es erledigt den Job nicht, sondern startet nur die asynchrone Erledigung. Daher wird im folgenden ForEach (in der .wr.DownloadString-Extension) .EndGetResponse aufgerufen, lange bevor das Ergebnis da ist. Das ist der wesentliche Unterschied zum Threading im QuerySearcher, wo .EndGetResponse() erst im Callback aufgerufen wird - wenn also die Daten da sind. In diesem Falle hier blockiert .EndGetResponse() nämlich solange, bis das Ergebnis da ist.
      Also zB. vom CurrentThread aus sind mit BeginGetResponse 4 Threads gestartet worden, dann gehts ins ForEach, und CurrentThread hängt nun vorm ersten EndGetResponse fest, bis die Daten da sind. Anschließend rennter gleich ins nächste EndGetResponse, denn es werden ja 4 requestInfos durchgenudelt.
      Der Trick ist jetzt: Wenn das zweite requestInfo schneller fertig ist als das erste, dann blockiert sein EndGetResponse ja nicht. Und wenn das 3. requestInfo wieder langsamer ist, blockiertes halt, aber nur um den Betrag, den es länger braucht als das erste requestInfo.
      Ergo: Die Gesamtwartezeit summiert sich nicht, sondern beträgt nur die Zeit des langsamsten Threads (parallel, eben).
      ("Fork-Join" heißtes übrigens weil die Threads schließlich zusammen laufen wie die Zinken einer Gabel).
      Jdfs. die erhaltenen DownloadStrings werden ebenso weitergeschickt wie beim QuerySearcher, nämlich zum MatchAnalizer.
      Ja, wenn dann alles getan ist wird QueryDone() gemeldet.

      MatchAnalizer

      VB.NET-Quellcode

      1. Public Class MatchAnalizer
      2. Public GetQueryRow As Func(Of QueryRow)
      3. Private _rgxTitle As New Regex("\<title\> (.*?) \| Kino \| Cinefacts\.de \</title\>")
      4. Private _rgxKurzInhalt As New Regex( _
      5. "\<li class=""\w+""\>\<h\d\>.+?KURZINHALT\</h\d>\</li\>\s*\<li class=""\w+""\>(.*?)\</li\>", _
      6. RegexOptions.Singleline)
      7. Private _rgxNewline As New Regex("\r\n|\n\r|[\n\r]")
      8. ''' <summary>
      9. ''' extrahiert mittels Regex dem Html-Quelltext gezielt Informationen, und added damit
      10. ''' der CineFactsDts.MatchDataTable eine DataRow
      11. ''' </summary>
      12. ''' <remarks></remarks>
      13. Public ReadOnly AnalizeRequest As Action(Of String) = _
      14. Sub(sHtml As String)
      15. Dim title = "<title-error>"
      16. Dim summary = "<summary-error>"
      17. Try
      18. Dim mt = _rgxTitle.Match(sHtml)
      19. If mt.Success Then title = mt.Groups(1).Value
      20. mt = _rgxKurzInhalt.Match(sHtml)
      21. If mt.Groups(1).Value = "" Then Return
      22. summary = _rgxNewline.Replace(mt.Groups(1).Value, "")
      23. summary = HtmlEscaper.Unescape(summary).Replace("<br />", Environment.NewLine)
      24. Finally
      25. SyncLock Me
      26. Program.Dts.Match.AddMatchRow(title, summary, GetQueryRow.Invoke)
      27. End SyncLock
      28. End Try
      29. End Sub
      30. End Class

      Im Vergleich mit den Threading-Geschichten von QuerySearcher und MatchSearcher ist dieser hier einfach: Der erste Regex ermittelt den Film-Titel und der zweite den KURZINHALT. Falls kein KURZINHALT gefunden wird, brichter ab, muß aber trotzdem noch durch den Finally-Block, sodaß auf jeden Fall eine MatchRow gebildet wird, und sei es mit Fehler-Werten.
      Der KURZINHALT, die summary muß nachgearbeitet werden, denn es ist Html-Text, der sich aus 3 Gründen in einer Textbox überhaupt nicht gut macht:
      1. zunächst müssen die 'stillen Newlines' entfernt werden: Html-Code kann ja beliebig Newlines enthalten, die im Browser nicht dargestellt werden - wohl aber inne Textbox. Besonders perfide, dass die HtmlCode-Newlines auch noch beliebig codiert sein können, also vbCr, vbCrLf, vbLf - sogar LfCr wäre zulässig. Daher bemühe ich auch zum Löschen dieser Newlines einen Regex (_rgxNewline).
      2. Dann muß natürlich <br /> durch Textbox-taugliche Newlines ersetzt werden - das kann auch olle String.Replace
      3. Zum dicken Ende müssen auch die Html-Escape-Sequenzen übersetzt werden: ö für &ouml; - ï für &iuml; - etc.) - das macht mein HtmlEscaper.

      Bischen Threading noch: Der SyncLock-Block gewährleistet, dass nicht mehrere Threads gleichzeitig eine MatchRow bilden, denn dann hätten diese MatchRows identische IDs. Also wenn ein Thread innerhalb des SyncLock-Blocks ist, können die anneren Threads den Block nicht betreten und müssen warten (geht ja schnell ;)).
      Weiter oben schon erwähnt: Der holende Out-Pin GetQueryRow, der nix abschickt, wie andere Out-Pins, sondern etwas einholt: nämlich die aktuelle QueryRow, die gebraucht wird, um eine MatchRow zu bilden.
      Denn (Thema Datenmodellierung): Jede MatchRow muß einer QueryRow untergeordnet sein (1:n - Relation: eine Query hat mehrere Matches).
      MatchAnalizer demonstriert deutlich den Nutzen von Separation of Concerns:
      Da die Zuständigkeit dieser Klasse genau in der Analyse des Htmls einer FilmDetail-Info-Site besteht, werden Erweiterungen zum Kinderspiel: Noch das Plakat runterladen? - einen Regex zufügen, noch die SchauspielerListe? - weiterer Regex, etc.

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Obwohl die App ohne Regex unmöglich wäre

      also ich habe schon oft html tags aus webseiten extrahiert und kein RegEx gebraucht

      was mich interresiert sind mehr die delegaten, mit denen habe ich mich nie wirklich mit beschäftigt

      und noch eine frage warum friert das programm kurz ein wenn man suchen lässt?(am pc kanns nicht liegen der i7 unterstützt multithreading, und win 7 auch)
      ok ich warte nochmal bis der rest drin ist...

      wollte noch sagen finde es eigentlich eine sehr gute idee erst hier zu posten,
      diskutieren zu lassen und dann zu den tutorials zu stellen !

      weiter so!

      mfg HeadShotHarp
      frmCineFacts
      Vom FrontEnd zeige ich nur den EBC-betreffenden Code (der Rest ist mehr oder weniger trivial):

      VB.NET-Quellcode

      1. Public Class frmCineFacts
      2. Public RequestQuery As Action(Of String)
      3. Public ReadOnly GetQueryRow As Func(Of QueryRow) = Function() QueryBindingSource.At(Of QueryRow)()
      4. Public ReadOnly QueryDone As Action = Sub() Me.BeginInvoke(UpdateGui, True)
      5. Private ReadOnly UpdateGui As Action(Of Boolean) = _
      6. Sub(requestDone)
      7. SplitContainer1.Enabled = requestDone
      8. QueryDataGridView.ReadOnly = Not requestDone
      9. For Each bs In {MatchBindingSource}
      10. bs.RaiseListChangedEvents = requestDone
      11. If requestDone Then bs.ResetBindings(False)
      12. Next
      13. lbStatus.Text = If(requestDone, "ready", "querying...")
      14. pgbQuerying.Visible = Not requestDone
      15. End Sub
      Es gibt den OutPin RequestQuery und die beiden InPins GetQueryRow und QueryDone. Deutlich wird mein Entwickler-Muster, welches OutPins als uninitialisierte Delegaten implementiert, und InPins als Readonly-Delegaten, mit anonymer Methode.
      Der InPin GetQueryRow ist ein Einzeiler der mittels der BindingSource.At-Extension die aktuelle QueryRow der QueryBindingSource abruft.
      Der QueryDone-InPin ruft den privaten UpdateGui-Delegaten auf, und zwar per Control.BeginInvoke, denn dieser Pin wird aus einem NebenThread heraus aufgerufen, und Nebenthread-Zugriffe auf Controls sind unzulässig und werden mit einer InvalidOperationException quittiert.
      Ich hätte UpdateGui auch traditionell als private Methode implementieren können, nur hätte ich dann mittels AddressOf einen Delegaten erzeugen müssen:
      Me.BeginInvoke(AddressOf UpdateGui, True)
      - aber wie ichs gemacht hab, kann ich den Delegaten auch gleich direkt angeben ;).
      UpdateGui disabled/enabled SplitContainer1, switcht die QueryDataGridView.ReadOnly-Property, disabled/enabled die Aktualisierung aller BindingSources (es gibt derzeit nur eine).
      Das mit den BindingSources ist auf Erweiterbarkeit ausgelegt, denn es ist durchaus nicht unwahrscheinlich, dass weitere DataTables im Dataset hinzukommen, die dann über weitere BindingSources an eine Anzeige gebunden werden - und diese weiteren BindingSources werde ich einfach dem BindingSource-Array {MatchBindingSource} hinzufügen können.
      Das disablen der BindingSources ist wichtig, denn das Dataset wird in dieser Anwendung in NebenThreads befüllt. Da Databinding eine Verbindung zw. DataTable und Gui herstellt, hätten wir ansonsten einen indirekten NebenThread-Zugriff auf Controls. Zwar wird das nicht mit einer Exception quittiert, aber das ist vlt. noch schlimmer, denn man würde sich damit ein vollkommen unkalkulierbares Verhalten einhandeln.
      Außerdem beschleunigt das Abkoppeln der BindingSources den Dataset-BefüllungsVorgang immens, da ansonsten sich die Grids bei jeder hinzukommenden DataRow neu zeichnen würden.
      Ja, zum Gui-updaten gehört auch eine Meldung in der Status-Zeile sowie das Einblenden einer Progressbar - da lassemer uns nicht lumpen :).

      Ich hopse einmal in die Helpers-Dll, um die BindingSource.At()-Extension zu zeigen:

      VB.NET-Quellcode

      1. ''' <summary> returnt die typisierte Datarow an aktueller Position - oder Nothing. </summary>
      2. <Extension()> _
      3. Public Function At(Of T As DataRow)(ByVal subj As BindingSource) As T
      4. Return DirectCast(DirectCast(subj.Current, DataRowView).Row, T)
      5. End Function

      Ein doppelter Cast: BindingSource.Current muß auf DataRowView gecastet werden, und dessen (untypisierte) DataRow auf den typisierten DataRow-Typ.
      Ziemlich gräßlich - daher habichdas als Extension verkapselt, dassichs nicht immer sehen muß ;)

      Class Program

      VB.NET-Quellcode

      1. Public Class Program
      2. Public Shared ReadOnly DataSetSaver As New DataSetSaver(Of CineFactsDts) With {.DataFile = "..\..\Other\CineFactsDts.txt"}
      3. Public Shared ReadOnly Dts As CineFactsDts = DataSetSaver.DataSet
      4. <STAThread()> _
      5. Public Shared Sub Main(ByVal commandLineArgs As String())
      6. Application.EnableVisualStyles()
      7. Application.SetCompatibleTextRenderingDefault(False)
      8. 'EBC-Komponenten (alles Atome) instanzieren...
      9. Dim frm = DataSetSaver.CreateForm(Of frmCineFacts)()
      10. Dim querySearcher = New QuerySearcher
      11. Dim matchSearcher = New MatchSearcher
      12. Dim analizer As New MatchAnalizer
      13. '...verknüpfen
      14. querySearcher.RequestQuery.Handles(frm.RequestQuery)
      15. matchSearcher.RequestMatches.Handles(querySearcher.RequestMatches)
      16. analizer.AnalizeRequest.Handles(matchSearcher.AnalizeRequest)
      17. frm.GetQueryRow.Handles(analizer.GetQueryRow)
      18. frm.QueryDone.Handles(matchSearcher.QueryDone)
      19. '(quasi auch EBC:) traditionelle Verknüpfung eine trad. Events
      20. AddHandler frm.FormClosing, DataSetSaver.HandleFormClosing
      21. ControlBehavior.InitGrids(frm) 'eine Art 'Skin' für DataGridViews
      22. Application.Run(frm)
      23. My.Settings.Save()
      24. End Sub
      25. End Class
      EBC erfordert einen Programm-Start im old-fashioned c#-Style. Das VB-Anwendungs-Framework ist in den Projekt-Eigenschaften deaktiviert, und stattdessen werden die notwendigen Einstellungen am Application-Objekt händisch gecodet.
      Dieses ist dem EBC-Prinzip geschuldet, dass alle Komponenten entweder Atome sind oder Platinen, und Platinen sollten neben dem Verknüppern ihrer untergeordneten Komponenten keine weitere Programm-Logik enthalten (Separation of Concerns).
      Da ein Form aber einen Haufen von Controls enthält, und endlos damit beschäftigt ist, User-Eingaben zu verarbeiten, kann es nicht zusätzlich die Aufgaben einer Mainboard-Platine übernehmen (wie es müsste, wenn man das VB-Feature eines StartForms nutzen würde).
      Das Verknüpfen eines InPins mit einem OutPin einer anderen Komponente erfolgt über meine Delegate.Handles()-Extension.
      Prinzipiell erfolgt damit dasselbe, wie wenn man mit AddHandler eine EventHandler-Methode einem Event zuweist.
      Also:

      VB.NET-Quellcode

      1. destComponent.InPinDelegate.Handles(srcComponent.OutPinDelegate)
      2. 'entspricht
      3. AddHandler srcComponent.OutEvent, AddressOf destComponent.InEventHandler
      Beachte, dass aus technischen Gründen die Reihenfolge von Source- und Destination-Komponente grad umgekehrt ist wie bei der AddHandler-Syntax (in dieser Hinsicht besteht mehr Ähnlichkeit zur Handles-Klausel von VB).

      Obige 5 Aufrufe der Handles-Extension stellen die codeseitige Umsetzung der im EBC-Diagramm konzipierten Kommunikationsstruktur dar.

      Hier noch der Code der Handles-Extension selbst (vier Überladungen):

      VB.NET-Quellcode

      1. <Extension()> _
      2. Public Sub [Handles](ByVal inPin As Action, ByRef outPin As Action)
      3. outPin = DirectCast([Delegate].Combine(outPin, inPin), Action)
      4. End Sub
      5. <Extension()> _
      6. Public Sub [Handles](Of T)(ByVal inPin As Action(Of T), ByRef outPin As Action(Of T))
      7. outPin = DirectCast([Delegate].Combine(outPin, inPin), Action(Of T))
      8. End Sub
      9. <Extension()> _
      10. Public Sub [Handles](Of T)(ByVal inPin As Func(Of T), ByRef outPin As Func(Of T))
      11. outPin = DirectCast([Delegate].Combine(outPin, inPin), Func(Of T))
      12. End Sub
      13. <Extension()> _
      14. Public Sub [Handles](Of T1, T2)(ByVal inPin As Func(Of T1, T2), ByRef outPin As Func(Of T1, T2))
      15. outPin = DirectCast([Delegate].Combine(outPin, inPin), Func(Of T1, T2))
      16. End Sub
      Delegate.Combine erzeugt einen neuen Delegaten, bei dem die AufrufeListen beider Delegaten zusammengeführt sind. Dieser Delegat wird (ByRef!) an den outPin zugewiesen.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Warfley schrieb:

      Obwohl die App ohne Regex unmöglich wäre

      also ich habe schon oft html tags aus webseiten extrahiert und kein RegEx gebraucht
      Hmm, kann sein, in anderen Anwendungsfällen.
      Aber ich habe einen Html-UnEscaper drin, der ersetzt alle Html-Escape-Sequenzen durch normal lesbare Sonderzeichen (öäü...) - Weil die KurzInfo ist ja Html-Text, muß aber in einer Textbox lesbar sein.
      Es gibt 99 verschiedene Escape-Sequenzen - du wirst wohl kaum 99mal String.Replace() aufrufen wollen. Also ohne Regex wirstedir einen ziemlich listigen Algo ausdenken müssen, der das Muster &...; erkennt, und durch einen ein Zeichen breiten Eintrag austauscht. (Wenndedas geschafft hast, hast du gewissermaßen Regex nachprogrammiert, also das berühmte Rad neu erfunden ;))
      warum friert das programm kurz ein wenn man suchen lässt?
      Echt? Mist, dann habichn Bug drinne, und keinen Ansatz, den zu finden, weil bei mir tuterdasnich :(
      Ich kann nur hoffen, dass du das Disablen aller Grids als Einfrieren mißverstehst. Aber das Disablen ist notwendig, damit der User nicht den Current-Query-Datensatz ändern kann, bevor die Suche fertig ist.

      Jdfs, deine erste Frage hat mich erinnert, dassichden Html-Escaper ja auch im Tut besprechen wollte, wegen der

      Regex-MatchEvaluator-Technik beim Textersatz:

      VB.NET-Quellcode

      1. Public Class HtmlEscaper
      2. Private Shared _UnescapeDic As New Dictionary(Of String, String)
      3. Shared Sub New()
      4. Using rd = New CompactReadWrite(New StreamReader("..\..\LocalHelpers\HtmlEscapeData.txt", True))
      5. For Each row In rd.ReadTable
      6. _UnescapeDic.Add(row(1), row(0))
      7. Next
      8. End Using
      9. End Sub
      10. Private Shared _rgxUnescape As New Regex("&.{2,6}?;")
      11. Public Shared Function Unescape(ByVal toEscape As String) As String
      12. Return _rgxUnescape.Replace(toEscape, _EscapeMatch)
      13. End Function
      14. Private Shared _EscapeMatch As MatchEvaluator = _
      15. Function(mt As Match)
      16. Dim s = mt.Value
      17. If _UnescapeDic.TryGetValue(s, s) Then Return s
      18. Dim i As Integer
      19. If Integer.TryParse(s.Substring(2, s.Length - 3), i) Then Return (Convert.ToChar(i))
      20. Throw New ArgumentOutOfRangeException("die EscapeSequenz '".And(s, "' ist nicht bekannt"))
      21. End Function
      22. End Class

      Die Shared Sub New() wird aufgerufen, wenn HtmlEscaper erstmalig aufgerufen wird. Dann werden aus einer Textdatei die Html-Escape-Sequenzen und ihre Entsprechungen in ein Dictionary eingelesen.
      Die Funktion Unescape ruft eine besondere Regex.Replace-Überladung auf, der ein Delegat (der MatchEvaluator) übergeben wird, welcher für jeden Match den ErsetzungsText ermittelt.
      Der MatchEvaluator guckt zunächst im Dictionary, ob es sich um eine der 99 benannten Escape-Sequenzen (&auml; etc.) handelt. Wennja, dann fertich.
      Andernfalls muß es sich um eine numerisch EscapeSequenz handeln (&#24; etc.). In diesem Fall wird der String vom 3. bis zum vorletzten Zeichen in einen Integer geparst, welcher in einen Char umgewandelt returnt wird.
      Antwort auf eine Frage von Rinecamo:

      Rinecamo schrieb:

      müsste ich wissen ob diese Helper wirklich notwendig sind um sowas umzusetzen.

      Helper-Code hilft halt. Also stellt Methoden bereit, der an mehreren Stellen sich als überaus praktisch erweisen kann.
      Wenn man also auf eine Helper-Methode verzichten will, muß man halt Ersatz schaffen, der dieselbe funktionalität übernimmt, of sogar an mehreren Stellen.
      Das würde den Code aufblähen und wäre damit eine Verschlechterung des Stils.

      Ist ganzngar deine Entscheidung, und musste im einzelfall treffen, welche HelperMethode du "rückbauen" möchtest.

      Beispiel zwei Helper-Methoden aus Helpers\0System\Extensions\ObjectX.vb

      VB.NET-Quellcode

      1. <DebuggerStepThrough()> _
      2. <Extension()> _
      3. Public Function Null(Of T As Class)(ByVal Subj As T) As Boolean
      4. Return Subj Is Nothing
      5. End Function
      6. <DebuggerStepThrough()> _
      7. <Extension()> _
      8. Public Function NotNull(Of T As Class)(ByVal Subj As T) As Boolean
      9. Return Subj IsNot Nothing
      10. End Function
      Diese Helferlein erleichtern mir das Abfragen auf Nothing ein bischen.
      In frmCineFacts, in

      VB.NET-Quellcode

      1. Private Sub btSearch_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btSearch.Click
      2. Dim rw = GetQueryRow()
      3. If rw.Null Then Return
      4. UpdateGui(False)
      5. For Each rwMatch In rw.GetMatchRows
      6. rwMatch.Delete()
      7. Next
      8. RequestQuery.Invoke(rw.Pattern)
      9. End Sub
      habichnun statt

      VB.NET-Quellcode

      1. If rw Is Nothing Then Return
      geschrieben:

      VB.NET-Quellcode

      1. If rw.Null Then Return
      Zugegebenermaßen eine geringfügige Erleichterung (aber schon merklich, denn Intellisense hilft dabei mehr als bei der Formulierung mittm Is-Operator). o.g. Helpers bieten aber noch paar annere Varianten, man könnte sogar sowas formulieren:

      VB.NET-Quellcode

      1. Private Sub btSearch_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btSearch.Click
      2. With GetQueryRow()
      3. If .NotNull Then
      4. UpdateGui(False)
      5. For Each rwMatch In .GetMatchRows
      6. rwMatch.Delete()
      7. Next
      8. Array.ForEach(.GetMatchRows, Sub(r) r.Delete())
      9. RequestQuery.Invoke(.Pattern)
      10. End If
      11. End With
      12. End Sub
      .Null() oder .NotNull sind also auch auf With-Block-Variablen anwendbar - das kann der Is/IsNot-Operator nicht ;)

      Du siehst (hoffentlich): Essis praktisch und kostet nix ;)
      Ich verwende grad diese beiden Helpers total viel, weil ist bisserl einfacher zu schreiben, und der Code ist absolut genauso eindeutig lesbar.
      Aber auch ein Rückbau wäre hier problemlos machbar, wenn mans lieber konventionell hat.

      vlt bezieht sich die Frage auch darauf, ob man prinzipiell ein Helpers-Projekt drinnehaben sollte.
      Würde ich unbedingt bejahen. Da gehört jeder besonders wiederverwendbare Code hinein, also was du über das aktuelle Hauptprojekt hinaus auch in anneren Projekten nutzen möchtest - also deine besten Stücke ;)
      Die Wiederverwendung erfolgt nicht per Copy&Paste, sondern indem das Helpers-Projekt einfach in annere Solutions ebenso eingebunden wird.

      Dieses Anwendungsdesign ist auch Bestandteil des Tuts.
      Also es geht nicht darum, dass man genau dieses Helpers-Projekt übernimmt, sondern ist v.a. eine Anregung, für die eigenen Projekte auf gleiche Weise ein Helpers-Projekt anzulegen, und vlt. ist mein Helpers-Projekt da ein ganz guter Startschuß, was die meisten der Methoden angeht, und die Art, wie die Helperlein in Dateisystem und Namespaces eingeordnet sind.
      Bei mir daheim habich die Helperei thematisch sogar noch ausdifferenziert: Ich habe ein allgemeines Helpers-Projekt, und wennich WinForms progge, binde ich zusätzlich zu die allg. Helpers noch meine "WinFormHelpers" ein, und für Wpf halt meine "WpfHelpers".

      Dassis prinzipiel genau das Konzept, was auch MS verwendet, etwa wenn man für eine WinForm-Anwendung die Bibliothek "System.Windows.Forms" einbinden muß, oder man muß "System.Xml.Linq" einbinden, wenn man mit Linq-Ausdrücken Xml verarbeiten möchte etc..

      Memo schrieb:

      Erfindest du nicht selber das Rad neu? Mit deiner Download-String Funktion?

      :thumbsup: Klaro! Steht mir ja auch zu - bei meinem Nick :thumbsup:
      Im Ernst: WebClient hat keinen Vorzug gegenüber WebRequest. Ebensowenig wie WebRequest mehrere Urls parallel anfordern kann, kanns auch der WebClient nicht - bei letzterem kriegste eine Exception, wenndes versuchst.
      Also muß man für jeden Request so oder so ein eigenes Objekt instanzieren, und da bevorzuge ich den WebRequest, denn der belegt wesentlich weniger Resourcen.