June 2003 - Posts
Eine kleine Erkenntnis für zwischendurch: Gerade habe ich versucht, eine Assembly aus dem GAC zu löschen. Zuerst wollte ich es über den Windows Explorer machen, der verweigerte aber den Dienst mit dem Hinweis xyz.dll could not be uninstalled because it is required by other applications. Hm... eine Anwendung, die die Assembly aber (gerade) benutzte, war mir nicht bekannt. Also habe ich es nochmal zu Fuß mit gacutil.exe probiert. Mit dem selben schlechten Erfolg - aber dafür einer etwas ausführlicheren Fehlermeldung: Unable to uninstall: assembly is required by one or more applications Pending references: SCHEME: ID: DESCRIPTION : .
Die Erklärung ist also: Wenn eine Assembly mit dem Windows Installer im GAC installiert wird, dann kann auch nur der Windows Installer sie wieder deinstallieren.
Das war im ersten Moment eine Überraschung für mich; ich hatte es nicht gewusst. Nach einem kleinen Moment des Nachdenkens ist es aber eine sehr positive Überraschung. Ich würde sagen: So und nicht anders sollte es sein. Hier ein Statement von Microsoft aus einem Chat dazu:
You should avoid using GACUtil to install managed assemblies for several reasons. First, you lose the ability to trace installation references in the GAC, unless you use the /r option to add tracking references. Second, GACUtil is part of the frameworks SDK, and is not guaranteed to be on all target machines - it's also not marked as redistributable, so you'd need to check the licensing to make sure you can actually ship it in your package. Third, MSI installation gets you repair and servicing for free, where GACUtil doesn't. Finally, GACUtil is more about populating an empty GAC from the start (as in the case of the frameworks installer) than about installing assemblies on a running system.
Es weist auch auf den allgemeinen Mechanismus hin, der da am Werk ist: Installationen von Assemblies in den GAC können mit Referenzen versehen werden. So wie bei COM-Objekten gibt es auch im GAC ein "reference counting". Jede Installation einer Assembly kann die Referenzen der Assembly hochzählen, jede Deinstallation wieder herunter zählen. (Das bedeutet auch, Sie können die selbe Assembly mehrfach im GAC installieren, aber sie liegt darin in Wirklichkeit nur einmal - allerdings mit mehreren Referenzen.)
Anders als bei COM ist der Referenzzähler einer Assembly keine simple Zahl (Anzahl der Referenzen), sondern eine Liste von Referenzen. Es gibt mehrere Arten von Referenzen (s. .NET Framework Doku zum Stichwort "gacutil.exe"). Eine Referenzenart ist z.B. FILEPATH, d.h. Sie können den Namen einer Assembly angeben, die die im GAC zu registrierende benötigt. Ein GacUtil.exe Aufruf sieht dann z.B. so aus:
gactutil.exe /i mylib.dll /r FILEPATH c:\myapp\myapp.exe MeineAnwendung
Auf FILEPATH folgt der vollständige Pfad zur Assembly, die von mylib.dll abhängig ist, "MeineAnwendung" ist nur eine Kurzbeschreibung dieser Referenz.
Die einfache Deinstallation mit gacutil.exe /u mylib ist jetzt nicht mehr möglich. Solange noch Referenzen an einer Assembly im GAC hängen, müssen Sie genau angeben, welche Referenz Sie deinstallieren wollen, z.B.
gactutil.exe /u mylib /r FILEPATH c:\myapp\myapp.exe MeineAnwendung
Erst wenn die Liste der Referenzen leer ist, wird die Assembly tatsächlich aus dem GAC gelöscht. Damit ist sichergestellt, dass nur derjenige, der eine Assembly im GAC installiert, sie auch wieder deinstallieren kann. Programminstallationen, die Assemblies im GAC ablegen, können also genauso robust und gefeit gegen unwillkürliche Löschungen "im System" sein, wie lokal installierte.
Ein wenig relativiert sich Microsofts Kommentar damit schließlich auch ein wenig: Sie können gacutil.exe schon anwenden, aber sie sollten zumindest darauf achten, dass Sie immer Referenzen angeben.
(Als Trost für alle, die die /r Option von gacutil.exe auch nicht gekannt haben: Sie ist schlecht dokumentiert. Im .NET Fx SDK ist sie zwar zu finden, aber online habe ich sie nicht auf der korrespondierenden Seite http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cptools/html/cpgrfglobalassemblycacheutilitygacutilexe.asp gefunden. Nur eine externe Quelle hat auch ein Beispiel geliefert: https://mailserver.di.unipi.it/pipermail/dotnet/msg00074.html.)
Barcelona ist eine schöne Stadt! Die TechEd gerade hier zum Jubiläum stattfinden zu lassen, ist eine gute Entscheidung :-) Ein paar Impressionen, um den Daheimgebliebenen den Mund wässrig zu machen...
Die Sonne brennt allerdings immer noch unerbärmlich. Aber ich habe wieder gelernt, laaaangsamer zu gehen. Zumindest draußen. Denn drinnen ist es überall Air Condition gekühlt. Der Schock beim Übergang erinnert manchmal sehr an die USA.
Nun ist es aber Montag und es kann endlich los gehen! Also auf zur Pl. Espana und die Registration gesucht... Doch halt: Beim Kongresszentrum an der Pl. Espana, da wo der Zauberbrunnen steht,
findet trotz all der TechEd Fahnen offensichtlich nichts statt :-( Die herbeiströmenden Teilnehmer (zwei Bekannte aus Deutschland habe ich gleich dort schon getroffen) werden erstmal in Busse verfrachtet und zum wirklichen Veranstaltungsort gekarrt. Ok, auch nicht schlecht, verlängert nur die Anfahrtzeit (insgesamt muss ich wohl knapp 1 Std vom Hotel aus rechnen), ist aber relativ bequem. (Der Aufwand des Transports für die bestimm 6000 Teilnehmer ist aber schon gigantisch. Dass sich das rechnet...?)
Auf dem Messegelände weisen freundliche Hostessen den Weg zur Registration. Für mich läuft alles problemfrei, Kollege Neno Loje allerdings wird zunächst nicht auf der Liste der Teilnehmer gefunden.
Heute am Montag sind nur einige allgemeine einführende Veranstaltungen wie "Enterprise Manageability" oder "Migration from VB6 to VB.NET". Alternativ gibt es noch ein paar Briefings für spezielle Gruppen (zu einer gehöre ich auch, scheue aber den langen Weg zurück in die Stadt zum Briefingort in einem Hotel am Hafen, weil ich später wieder hier am Veranstaltungsort sein muss).
Statt Briefing schaue ich daher mal den Veranstaltungskalender an. Unendlich viele Sessions buhlen um meine Aufmerksamkeit. 13 Sessions laufen immer parallel, 6 Timeslots gibt es pro Tag = knapp 80 Sessions pro Tag, d.h. mehr als 300 Sessions insgesamt. Davon kann jeder Teilnehmer aber nur vielleicht 8-10 jeden Tag sehen (jenachdem, ob er Sessions immer komplett mitmacht). Am Ende wären das also vielleicht 35-40 Sessions. Auf der einen Seite ist das eine Menge und Information Overload droht. Auf der anderen Seite aber auch wieder nicht so viel, weil es nur ca. 13% alles Sessions sind. Hm... Ich grüble, ob das so ideal ist. Lohnen da Aufwand und Nutzen?
Statt Quantität aber jetzt zur Qualität bzw. den Inhalten. Das Programm ist unheimlich vielfältig. Eine gute Übersicht bietet aber das kleine Faltprospekt für den Brustbeutel. Eine erste Durchsicht ergibt für mich ca. 4-5 grundsätzlich interessante Sessions pro Timeslot. Puh, wieviele ich davon auch nur grob hören kann??? Mal schauen... Spannende Vorträge sind in jedem Fall zu erwarten, z.B. "Building a Data Source independent DAL, a good idea?" oder "Microsoft and Open Source" oder "Writing Faster Managed Code".
Von einem anderen Blickwinkel aus betrachtet, frage ich mich aber auch, wo die "inhaltliche Linie" der ganzen Vorträge liegt. Sicherlich hat die TechEd den Anspruch, Vielfalt in vielen Bereichen zu bieten (von .NET Framework über BizTalk Server bis zum PDA Programmierung). Aber diese Vielfalt scheint manchmal auch für eine gewisse Fokuslosigkeit zu sorgen. Einführende Vorträge zu ADO.NET oder XML Schema oder auch fortgeschrittene Themen wie "IPSec and NAT-Ts: Finally in Harmony?" finde ich irgendwie fehl am Platze. Der Eiertanz der Organisatoren zwischen Anfänger-Fortgeschrittene- und Developer-IT-Pro-Themen scheint offensichtlich. Das ist schade. Mehr Klarheit in der Entscheidung, ein deutlicheres Profil könnten doch bestimmt für mehr Teilnehmer (oder am Ende höhere Zufriedenheit) sorgen.
Aber naja, schaun mer mal, was es ab morgen denn tatsächlich zu sehen und zu hören gibt. Ich bin gespannt.
Bin nun schon in Barcelona zur TechEd angekommen. 4 Tage Urlaub vor dem grossen Event tun gut. Getruebt wird die Entspannung nur durch einen Artikel, den ich noch schreiben muss. Puh... Aber nach des Tages Hitze ist spaet abends dafuer immer noch Zeit.
Barcelona ist eine schoene Stadt. Schoene Jugendstilbauten, mediterranes Flair, kleine Gassen, grosse Avenuen, huebsche Plaetze. Eine gute Wahl als Veranstaltungsort fuer die TechEd.
Ein paar Tipps fuer diejenigen, die noch zur TechEd anreisen:
-Am Flughafen gibt es einen Bus in die Innenstadt zum Plaza Catalunya. Kostet 3,45 EUR und faehrt regelmaessig direkt vor dem Flughafenterminalausgang ab. Vom Plaza C. gehen dann viele U-Bahnen und F.G.C. Zuege (soetwas wie S-Bahn?) in alle Richtungen. Ausserdem ist das Busnetz gut ausgebaut. Die Anreise zum Hotel ist also immer recht schmerzfrei und guenstig. Zur Not ein Taxi nehmen. Sie sind nicht teuer, aber zuverlaessig. (Keine Angst, man wird hier nicht wie in Italien uebers Ohr gehauen :-)
-Der oeffentliche Personennahverkehr ist sehr gut organisiert und preiswert. Am besten eine 5 Tage-Karte fuer 17 EUR loesen und nicht mehr ueber Bus/Bahn-Preise nachdenken, sondern einfach fahren.
-Steckdosen in Spanien sind wie in Deutschland. Spannung auch.
-Waehrung ist der Euro. Das ist sehr bequem! So macht Globalisierung auch mal Spass :-) Die Preise sind im Supermarkt und auch in Restaurants/Bars voellig ok. Keine speziellen Touripreise, besonders wenn man ein wenig von den ausgetretenen Pfaden abweicht.
-Das Wetter ist sehr warm, geradezu feuchtheiss. T-Shirts sind die angemessene Oberbekleidung. Aber Achtung: ueberall gibt es Air Condition. Der Schock zwischen drinnen und draussen ist vorprogrammiert.
-Die TechEd findet am Plaza Espanya statt. Die U-Bahnstation ist gleichnamig.
-Vorsicht Taschendiebe! Gerade im Zentrum sind viel Touristen unterwegs und bilden ein weites Beaetigungsfeld fuer Taschendiebe. Am besten also nicht wie ein typischer Tourist aussehen (kurze Hose und Birkenstocksandalen mal daheim lassen :-) und einen Hueftbeutel umschnallen fuer Geld und Kreditkarte. Gibt es am Flughafen in Deutschland fuer 12 EUR zu kaufen und ist sehr bequem. Auch Handtaschen vermeiden, wenn es geht.
Gute Reise! Vielleicht sehen wir uns ja auf der TechEd.
Gerade habe ich Clemens Vasters' Blog Eintrag vom 24. Juni gelesen und erlaube mir einfach mal zu sagen: Ich empfinde genau das Gegenteil :-)
Clemens sagt I start to think that these types of talks [er meint Vorträge über Konzepte statt Technologien] just make more sense to conference attendees than "coding sessions" und ich behaupte einfach mal: Nein, Konferenzen brauchen mehr Code und weniger Slides, mehr Code und weniger Erklärungen. Denn es stimmt everyone can pick up a "how-to" book, [and] read the reference material, aber eben nicht jeder hat die Muße to poke around in samples at home!
Entwickler wollen eben nicht das Gelesene ausprobieren, sondern sehen wie es geht. Sie wollen Technologien in der Anwendung (!) demonstriert bekommen, in halbwegs vollständigen und realitätsnahen Szenarien. Und das leistet weder ein konzeptioneller Talk, noch einer der bisher üblichen "code a little, ppt a little" Vorträge.
Entwickler fordern heute (mehr als früher) weniger Erklärung von Technologien an sich, sondern vielmehr den Beweis ihres sinnvollen Einsatzes. Gerade mein letzter TechTalk und die ASP.NET Konferenz haben mir das wieder bestätigt. Meine "no slides, just code" Talks haben sehr gutes Feedback bekommen. Very bad, there was no demo stand jedenfalls nicht in den Feedbackbögen :-)
Allerdings: Vorträge, in denen nur Codiert wird, sind aufwändiger vorzubereiten und auch fordernder bei der Durchführung für Sprecher und Publikum. Dem Publikum bieten sie angesichts von Bücherbergen mit Titeln wie "Advanced XYZ" oder "Insider ABC" oder "QWRTZ for Dummies" einen Mehrwert, den sie nicht durch Lesen erhalten: Technologien werden im Zusammenhang vorgestellt und mit der emotionalen Atmosphäre einer Live-Performance verknüpft. Damit lohnt sich plötzlich eine Anwesenheit bei einem Event wieder mehr.
An ideal conference inspires ist auch meine Meinung. Die Inspiration kann und soll aber auf zwei Ebenen stattfinden: sachlicher und emotionaler. Eine Inspiration in und zur Sache kann (und soll) auch eine Live-Coding-Vortrag liefern (eben weil er sonst nie gezeigte Zusammenhänge herstellt). Und die emotionale Inspiration stellt sich (hoffentlich) ein, weil bei einem Vortrag mit viel Code die Konzentration bei den Teilnehmern höher ist, das Engagement des Sprechers größer sein muss und sich somit eher ein "Flow Erlebnis" einstellt.
Aber nichts für ungut, Clemens :-) Am Ende bin ich auch (!) seiner Meinung: Mehr konzeptionelle Vorträge tun ebenfalls Not. Denn Technologien nützen nichts, wenn man die Grundlagen, die Hintergründe, die Konzepte nicht versteht. Und das kommt in vielen Vorträgen heute zu kurz. Bei Erkenntnis eines Konzeptes fallen plötzlich viele Technologiepuzzlesteine an ihre Plätze, ein Aha-Erlebnis stellt sich ein, Inspiration durch vertieftes Verständnis ist das Resultat.
Insofern: Allemal Clemens und ich - und auch die anderen Microsoft Regional Directors -, wir sollten uns aufgerufen fühlen, neue Präsentationsformen den eingefahrenen Stilen entgegen zu setzen. An beiden Enden des Codemengenspektrums :-)
Clemens' konzeptionelle Vorträge, "no slides, just code" und auch die Developer LAN Party von Christof Sprenger und mir sind Versuche in dieser Richtung. Ich/wir bin/sind gespannt auf das Feedback aus der Community.
Objektserialisierung ist mit dem .NET Framework denkbar einfach. Sie müssen eine Klasse nur mit einem Attribut als Serialisierbar markieren. Die Serialisierung erfolgt dann entweder implizit, wenn eine Instanz einer solchen Klasse über eine Remoting-Grenze bewegt wird. Oder Sie führen sie explizit durch, weil Sie z.B. das Objekt speichern oder per TcpClient verschicken wollen. Für die (De)Serialisierung sind Formatter-Objekte zuständig.
So weit, so gut. Im Rahmen des Projektes der Developer LAN Party (www.devlanparty.de), die Christof Sprenger und ich am 20./21.6. in der TU München veranstaltet haben, funktionierte die Serialisierung jedoch nicht so einfach.
Innerhalb der von den Teilnehmern zu realisierenden Anwendung sollte das Model-View-Controller (MVC) Pattern angewandt werden. Ein Model hielt Daten, ein Controller war verantwortlich für deren Manipulation durch die Methoden der Model Objekthierarchie und ein View sollte den Zustand des Model nach Veränderung anzeigen.
Das Pattern zu realisieren war auch kein Problem. Das Model feuerte einen Event, um angeschlossene Views zu informieren, dass es vom Controller verändert wurde. Eine Implementation "nach dem Buch":
Public Class Model
Public Event OnChange(...)
...
Public Sub DoSomething(...)
...
RaiseEvent OnChange(...)
End Sub
End Class
Dann aber sollte das Model bei Bedarf auch noch an einen anderen Prozess verschickt werden. Dafür wurde es als serialisierbar markiert:
<Serializable> _
Public Class Model
Public Event OnChange(...)
...
End Class
Bei der Serialisierung trat dann aber ein Fehler auf:
imports System.Runtime.Serialization.Formatters.Binary
...
private withevents _m as new Model()
...
_m.DoSomething(...)
...
dim ms as New IO.MemoryStream
dim s as New BinaryFormatter
s.Serialize(ms, _m)
Serialize() meldete, dass das Formular, in dem _m mit WithEvents deklariert war, nicht serialisierbar sei! Was war los?
Die Lösung liegt im Verständnis, wie Events im .NET Framework implementiert sind. Events sind Delegate-Typen und eine Event-Deklaration in einer Klasse fügt ihr eine weitere Member-Variable hinzu. Die Model-Klasse sah also intern etwas so aus:
Public Delegate Sub OnChangeDelegate(...)
<Serializable> _
Public Class Model
Public onChange as OnChangeDelegate
...
Public Sub DoSomething(...)
...
onChange(...)
End Sub
End Class
Und im Formular passierte folgendes, um den Eventhandler an das Model zu binden:
Private _m as new Model()
...
Private Sub _m_OnChange(...)
End Sub
...
_m.onChange = new OnChangeDelegate(AddressOf _m_OnChange)
Damit enthielt die Model-Instanz in onChange einen verweis auf das Formular in Form eines Delegate, der nicht nur die Adresse der Aufzurufenden Funktion speichert, sondern auch einen Verweis auf das Objekt, auf dem sie aufgerufen werden soll.
Da die Serialisierung rekursiv verläuft und immer einen ganzen Objektbaum ausgehend von der an Serialize() übergebenen Wurzel versucht zu serialisieren, stolperte sie natürlich auch über onChange und stellte fest, dass das angebundene Formular eben nicht serialisierbar war.
Ein Ausschluss der Event-Definition von der Serialisierung war allerdings nicht (!) möglich:
<Serializable> _
Public Class Model
<NonSerialized> _
Public Event OnChange(...)
...
End Class
Die Lösung brachte erst eine Implementation des ISerializable-Interface:
<Serializable> _
Public Class Model
implements ISerializable
Event OnChange(...)
Private _myMember as Integer
public sub new(info as SerializationInfo, context as StreamingContext)
_myMember = info.GetInt32("myMember")
End Sub
Public Sub GetObjectData(ByVal info As System.Runtime.Serialization.SerializationInfo, _
ByVal context As System.Runtime.Serialization.StreamingContext) _
Implements System.Runtime.Serialization.ISerializable.GetObjectData
info.AddValue("myMember", _myMember)
End Sub
End Class
In der Methode GetObjectData() des Interface wurden nur die wirklich zu serialisierenden Member-variablen des Objekts in den Serialisierungsdatenstrom geschrieben und in einem speziellen Konstruktor auch nur genau diese Member wieder ausgelesen. Der versteckte Member für den Eventhandler-Delegate wurde also ausgespart. Damit war das Model problemfrei zwischen Prozessen übertragbar.
Today I have received a number of requests from developers who asked me to post English Web Log messages.
As much as I feel flattered by the interest in my postings and of course would like to comply with the repeated request, I feel, on the other hand, I should not. For one simple reason: my target audience is the German or German-speaking developer community.
I´m living in Germany, I´m working in Germany, Germany (or the German-speaking regions: Germany, Austria, Switzerland) is the third (or even second?) largest software market, and I´m not very eager to increase my travelling abroad much for consulting/speaking engagements.
So let me apologize to all you English speaking developers: I can empathize with you, and I will reevaluate my standpoint in a couple of months (if I manage to keep on blogging for that long).
But for now I feel I serve my target audience much better by providing them information in their own language. Knowing the German developer community from many events for many years I simply know, although they might be able to read English technical literature, they very much prefer to read in German. That´s why I´m blogging in German.
I full well understand, this will limit the reach of "my message", but I´m willing to suffer the consequences. As you might guess, I´m constantly weighing the pros and cons. Let´s see, what I´ll be thinking in a couple of months. Stay tuned...
Um Assemblies zur Laufzeit zu ersetzen, d.h. eine neue Version (quasi) jederzeit einbinden zu können, müssen nicht mehr benötigte Assembly-Versionen aus dem Speicher entfernt werden. Darum ging es gestern.
Wie aber weiß ein Prozess, dass sich überhaupt eine Assembly verändert hat und er mit einer neuen Version arbeiten soll? Am einfachsten wäre es, wenn man die bisherige Assembly auf der Festplatte durch eine neue ersetzte. Das könnte der Prozess mit einem FileSystemWatcher feststellen:
private withevents _fsw as IO.FileSystemWatcher
...
_fsw = new IO.FileSystemWatcher("c:\myapp\bin", "myassembly.dll)
_fsw.NotifyFilter = IO.NotifyFilters.LastWrite
_fsw.EnableRaisingEvents = true
...
Private Sub _fsw_Changed(ByVal sender As Object, _
ByVal e As System.IO.FileSystemEventArgs) _
Handles _fsw.Changed
' tausche Assembly aus...
End Sub
Solange eine Assembly allerdings geladen ist, kann sie nicht auf der Festplatte ersetzt werden. Die Datei ist gesperrt.
Um das zu vermeiden, kann eine AppDomain jedoch angewiesen werden, Assemblies nicht im Original zu laden, sondern sie vorher zu kopieren. Die kopierten Assemblies nennt der .NET Framework "Shadow Copies". Sie werden für die Anwendung transparent in einem temporären Verzeichnis vor dem Laden erzeugt. Dort sind sie zwar immer noch gesperrt, aber das ist dann unerheblich.
Denn am ursprünglichen Ort können sie überschrieben werden.
Die Dokumentation zu Shadow Copies ist leider nicht ausführlich und so hat es mich einige Zeit gekostet, die "wahre Funktionsweise" herauszufinden.
Beim Erzeugen einer AppDomain werden ihr einige Pfade mitgeteilt, in denen sie zu ladende Assemblies suchen soll:
dim info as New AppDomainSetup()
with appdomain.CurrentDomain.SetupInformation
info.ApplicationBase = "c:\inetpub\wwwroot\myapplication"
info.PrivateBinPath = "bin"
end with
ApplicationBase ist das Wurzelverzeichnis der Anwendung, also meist das, wo die EXE-Datei liegt. Bei Web-Anwendungen ist es allerdings das Verzeichnis für die Virtual Root der Applikation.
PrivateBinPath enthält eine durch ";" getrennte Liste von relativen (!) Pfaden für Verzeichnisse, in denen unterhalb (!) der ApplicationBase vom CLR Lader auch nach Assemblies gesucht werden soll. Ist die Liste leer, dann werden nur Assemblies im ApplicationBase-Verzeichnis gefunden.
PrivateBinPath könnte also z.B. gesetzt werden auf ".", "bin" oder "bin;plug-ins", aber nicht auf "c:\mylibraries" oder "..".
Um mit Shadow Copies zu arbeiten, sind noch zwei weitere Eigenschaften einer AppDomain zu setzen. Mit
info.ShadowCopyFiles = "true"
wird der Mechanismus eingeschaltet. (Achtung: Der zugewiesene Wert ist ein String ("true" oder "false"), kein Boolean-Wert!)
Ohne weitere Angaben werden jetzt alle Assemblies in der ApplicationBase und in den PrivateBinPath-Verzeichnissen nur als Kopien geladen. D.h. in diesen Verzeichnissen sind sie nun nicht mehr gesperrt und können ersetzt werden.
Falls allerdings nur Assemblies aus einer Untermenge von PrivateBinPath-verzeichnissen als Shadow Copies geladen werden sollen, kann das über die Property ShadowCopyDirectories angegeben werden, z.B.:
info.ApplicationBase = "c:\myapplication"
info.PrivateBinPath = "plug-ins"
info.ShadowCopyDirectories = "c:\myapplication\plug-ins"
ShadowCopyDirectories ist optional und nimmt auch wieder eine Liste von ";"-separierten Verzeichnissen auf. Aber Achtung: Hier sind absolute (!) Pfade anzugeben. Leider ist das der .NET Framework Dokumentation nicht zu entnehmen und es hat mich einige Zeit gekostet, darauf zu kommen, als meine Tests nicht funktionierten.
Die Schritte für den Austausch von Assemblies zur Laufzeit - so wie es in ASP.NET funktioniert - sind damit klar:
-Assemblies in eine separate AppDomain laden und nur über ein Proxy-Objekt darauf zugreifen.
-AppDomain mit Shadow Copies arbeiten lassen.
-FileSystemWatcher einrichten, um festzustellen, wann sich Assemblies verändert haben.
-AppDomain entladen und neu aufbauen mit veränderten Assemblies.
Die Realisierung gerade des letzten Schrittes ist aber natürlich nicht trivial. Wie kann Ab- und Aufbau einer AppDomain vor Client-Code verborgen werden? Auch das Proxy-Objekt wird ja verworfen. Die Entkopplung muss also eigentlich noch stärker sein. Vielleicht über einen TCP/IP-Kanal?
Und auch noch andere Fragen sind zu klären: Wie können mehrere zusammengehörige Assemblies ausgetauscht werden? (Das ist selbst bei ASP.NET nicht wirklich möglich.) Und was soll passieren, wenn neue Assembly-Versionen eigentlich einen Anwendungsneustart erfordern?
Hm... darüber muss ich ein andermal nachdenken.
Seit meinem TechTalk über "Kompilieren statt Interpretieren" treibt mich die Frage um, wie Assemblies zur Laufzeit ausgetauscht werden können.
Ausgangspunkt der Überlegung sind zwei Probleme:
- Assemblies können nicht einzeln aus einem Prozess entladen werden.
- Assemblies sind auf der Festplatte gesperrt, sobald sie in einem Prozess geladen wurden.
Für das erste Problem ist der Lösungsansatz einfach: Assemblies werden in AppDomains geladen und AppDomains können auch wieder entladen werden. Damit verschwinden auch die darin geladenen Assemblies aus dem Speicher. Um Assemblies nach Gebrauch entsorgen zu können, müssen sie also nur in eine eigene AppDomain geladen werden.
dim info as New AppDomainSetup()
with appdomain.CurrentDomain.SetupInformation
info.ApplicationBase = .ApplicationBase
info.PrivateBinPath = "."
end with
dim app as AppDomain
app = AppDomain.CreateDomain("MyDomain", _
AppDomain.CurrentDomain.Evidence, info)
dim c as Class1
c = app.CreateInstanceAndUnwrap("MyLib", "MyLib.Class1")
...
appdomain.Unload(app)
Class1 in diesem Beispiel muss allerdings von MarshalByRefObject abgeleitet sein, da sonst kein Cross-AppDomain-Aufruf möglich ist.
Allerdings tritt auch dann nicht der gewünschte Effekt ein, da die aufrufende AppDomain ebenfalls die Assembly MyLib.dll laden muss, um den Proxy für die Class1-Instanz in der neuen AppDomain zu erzeugen.
Das bedeutet: Um eine Assembly mit einer AppDomain auch wieder zu entladen, darf außerhalb der AppDomain keinerlei Bezug zu der Assembly existieren! Um mit Typen aus der Assembly zu arbeiten, müssen alle Interaktionen durch Objekte laufen, deren Assemblies nicht entladen werden müssen. Für das obige Beispiel würde das z.B. bedeuten:
dim c as Class1Proxy
c = app.CreateInstanceAndUnwrap("MyApp", "MyApp.Class1Proxy")
Die Klasse Class1Proxy könnte zum Beispiel in der aufrufenden Assembly definiert sein, die schon in der aufrufenden AppDomain geladen ist. Diese Assembly würde dann zwar auch noch einmal in die neue AppDomain geladen, aber das macht nichts. Sie ist ja ohnehin schon im Speicher und soll daraus nicht verschwinden.
Zu beachten ist dann allerdings, dass innerhalb von Class1Proxy kein Typ aus der "wahren" zu ladenden Assembly an ihrer Schnittstelle auftaucht. Also keine Member-Variable und kein Parameter eine Methode darf von einem Typ der zu ladenden Assembly sein, denn sonst wird sie auch in der aufrufenden AppDomain benötigt, um den Proxy zu bauen.
Problem 1 ist damit gelöst: Zur Laufzeit kann ein Programm nicht mehr benötigte Assemblies los werden. Und Problem 2? Dazu morgen mehr.
Für den Einstieg in das Schreiben für die dotnetpro hatte ich neulich als Thema gewählt, Databinding für ADODB.Recordset Objekte zu realisieren. Ich wollte Recordsets wie DataTables an WinForms/WebForms UIs binden können. Complex und simple binding wollte ich realisieren.
Nach 1-2 Tagen Herumexperimentieren habe ich das Thema allerdings (vorläufig?) zu den Akten gelegt. Zwei Wege schienen mir grundsätzlich gangbar:
- Erstellung eines Containers, der selbst gebunden werden kann. Dem Container würde das Recordset zugewiesen und er würde die für´s Databinding notwendigen Schnittstellen implementieren-
- Erstellung eines Containers, der wie ein XmlDataDocument an ein DataSet gekoppelt werden kann. Bei der Kopplung würde das Recordset im Container in das DataSet kopiert (über OleDbDataAdapter.Fill()) und anschließend über das DataSet gebunden. Veränderungen am DataSet würden über Events im Container abgefangen und an das Recordset weitergeleitet werden - und umgekehrt.
Lösung 2. schien mir schwieriger und ich habe sie wg. der Kürze der Zeit (es war leider ein "Schnellschuss-Artikel" notwendig) nicht ausprobiert.
Lösung 1. habe ich angefangen und ein gutes Stück voran getrieben. Die ganze Zeit habe ich mich allerdings nicht wohl gefühlt. Ein Zeichen dafür war wiederholtes Nachdenken darüber, ob ich für die Bindung die Arten der erlaubten Cursor einschränken sollte. Am liebsten wäre mir gewesen, mich auf disconnected Recordsets zu beschränken. Das wäre aber dem Zwecke einer nahtlosen Bindung von Recordsets, also einer Gleichstellung von ADODB mit ADO.NET in dieser Hinsicht, zuwider gelaufen. Also habe ich versucht, quasi alle Cursor zu erlauben und maximale Databinding-Funktionalität bereit zu stellen (d.h. listen, einzelne Sätze binden und bearbeiten).
Unterm Strich war der Erfolg jedoch bescheiden bzw. der Aufwand schien angesichts der Einschränkungen bei bestimmten Cursorn (z.b. keine Kenntnis über Anzahl der Sätze bei Dynamic Cursor) zu hoch. Mein persönliches Resultat: Gerade weil ein ADODB.Recordset ein Cursor ist (und keine Objekte enthält und eigentlich also auch nicht frei positionierbar ist), wiederspricht es dem Programmiermodell des .NET Databinding. Beide passen einfach nicht gut zusammen. Symbolisiert wird das durch die Eigenschaften Current und Position des Currency Managers für eine Datenquelle. Current verweist auf das aktuelle Objekt in einer Liste von Objekten. Ein Recordset enthält aber weder Objekte, noch sind sie (immer) direkt über einen Index (AbsolutePosition) adressierbar.
Recordset-Databinding in .NET beim Anspruch voller Funktionalität ist daher quasi ein Widerspruch in sich. In Teilen ließe es sich aber wohl doch realisieren, z.B. so wie auch DataReader in ASP.NET bindbar sein.
Als neues Thema für den dotnetpro Artikel habe ich nun die Realisierung einer Komponente zur Speicherung von Anwendungseinstellungen in Angriff genommen.
More Posts