by wmn
7. June 2011 19:30
Viele FoxPro-Entwickler stehen vor dem Problem große Anwendungen in die schöne neue Welt <G> zu migrieren. Häufig steht schon fest, daß die neue Entwicklungsumgebung .NET und damit Silverlight oder WPF oder ASP.NET sein wird. Aber auch für jede andere Web-Technologie ist der hier beschriebene Weg verwendbar.
Soweit so gut. In den meisten Fällen steht man aber vor der fast unlösbaren Aufgabe eine große Anwendung umzustellen. Meist hat man dafür weder die Entwicklungs- noch die Testkapazität geschweige denn das notwendige Budget. Somit bleibt nur, diese Umstellung Schritt für Schritt vorzunehmen. Da sich die Umstellungsphase auch über einen sehr langen Zeitraum hinziehen kann, sind Ideen für eine nahtlose Integration zwischen alten und neuen Systemteilen dringend gefragt.
Vor etwa zwei Jahren habe ich hierzu schon einmal das Windows Messaging im Rahmen eines Vortrags gezeigt. Dabei “kommunizieren” eine FoxPro-Anwendung und eine .NET-WPF-Anwendung über das Windows-Messaging miteinander. Der Anwender springt dabei - kaum merklich – zwischen alter und neuer Welt hin und her.
Heute stelle ich hier eine noch reibungslosere Integrationsmöglichkeit zwischen FoxPro-Anwendungen und Silverlight vor.
Die FoxPro-Beispielmaske:
Den Kern der Idee bildet das Internet-Explorer-Control (Active-X-Control!). Dieses Steuerelement bietet uns alle entscheidenden Funktionen um Silverlight-Masken anzuzeigen. Ein Beispielaufruf ist hinter dem Click-Event der Buttons implementiert:
this.olecontrol1.navigate2(“http://localhost:58226/slFoxIntegrationTestPage.aspx#/home”)
Die Silverlight Anwendung
Der Trick für eine nahtlose Integration von Silverlight in eine FoxPro-Anwendung liegt im sogenannten “Deep-Linking”, das Silverlight seit der Version 3.5. bietet. Hierbei kann man eine Silverlightanwendung direkt mit einem Aufruf einer bestimmten Maske starten. Am einfachsten kann man sich eine solche Anwendung über das “Silverlight Navigation Application”-Projekttemplate im Visual-Studio anlegen.
Ich habe die Optik der Startseite etwas angepasst sodaß man hier die Kopfzeile nicht mehr sieht. Diese brauchen wir für unser Beispiel einfach nicht mehr, da FoxPro für die Navigation in der Anwendung verantwortlich sein soll. Die Mainpage der Silverlight-Anwendung sieht dann so aus:
Nicht gerade spannend aber für unsere Zwecke völlig ausreichend. Kern dieser Seite ist jetzt nur noch das Frame-Control, das unsere Masken “hostet”, die wir von FoxPro aus direkt starten wollen. Hier der Auszug aus dem XAML der MainPage.xaml-Datei:
Damit sind die wesentlichsten Komponenten fertig gestellt. Wenn man jetzt die Silverlight-Anwendung startet bekommt man im Internetexplorer auch die korrekte Adresse der Silverlight-Anwendung auf seinem eigenen Rechner angezeigt. Diese kann man für den ersten Test einfach kopieren und nach FoxPro übertragen.
Wie kann man nun von FoxPro aus bestimmte Seiten direkt starten? Nun das geht direkt über die Startseite, die man mit entsprechenden Parametern aufrufen kann:
http://localhost:58226/slFoxIntegrationTestPage.aspx#/about?CustNo=1
Hierbei wird der Parameter “/about” über den URI-Mapper der MainPage in die Silverlightseite “/Views/About.xaml” umgemappt und dieses Control wird dann in dem obigen Frame-Control angezeigt.
Wie kann man dieser Maske nun von FoxPro aus Parameter übergeben? Auch das sieht man in der oben angebenen Adresse. Das “?” ist der Marker an dem Parameter intern unterschieden werden. In dem angezeigten Beispiel wird die CustNo mit dem Wert 1 übergeben. Auslesen kann man diesen Parameter dann auf der Silverlightseite z.B. im Codebehind der Maske über folgenden Aufruf:
Der Zugriff auf den Parameterwert ist sehr einfach, da VisualStudio uns hier ganz bequem zugreifen lässt:
Stellt sich natürlich noch die Frage, wie man Ergebniswerte von Silverlight nach FoxPro zurückgeben kann. Auch das ist eigentlich recht einfach solange es sich um wenige einzelne Informationen handelt. Der Trick ist hierbei eine Dummy-URl zu der man von Silverlight aus navigiert, um zurück zur FoxPro-Anwendung zu kommen. In folgendem Beispiel werden gefundene Daten zu einer URL zusammengesetzt und dann dorthin navigiert:
Natürlich gibt es diese Webseite nicht wirklich. Hier sollte jetzt eigentlich auch ein entsprechender Fehler im Explorer-Control erscheinen. Damit das nicht passiert “fangen” wir diese Dummy-URL im Inernet-Explorer-Control in der FoxPro-Maske ab.
Hierzu habe ich am OCX-Control die Methode BeforeNavigate2 überschrieben. In diese wird die Dummy-URL als Parameter hereingegeben. Dadurch, daß ich den Parameter Cancel auf .t. gesetzt habe, wird die Navigation abgebrochen und der Aufruf der Seite quasi beendet.
Was aber tun, wenn man eine ganze Ergebnisliste von Silverlight nach FoxPro übergeben will? Die Übergabe per URL ist dafür sicher nicht geeignet. In diesem Fall muss man etwas mehr Aufwand betreiben. Der einfachste Weg hierfür geht sicherlich über XML-Dateien, die man im Isolated-Storage ablegt und deren Dateiname man als Parameter an Foxpro zurückgibt.
Der hier beschriebene Weg ermöglicht es - auch große Anwendungen - Stück für Stück umzustellen ohne dass der Anwender davon etwas merkt. Wenn Farben und Schriftarten sowie die Maskengestaltung angepasst wird, ist es für den Benutzer kaum auseinander zu halten, ob er sich in FoxPro oder in Silverlight bewegt. Wenn man sich die Implementierung der Serviceschnittstelle sparen will, kann man sicherlich auch WPF-Anwendungen auf die gleiche Art integrieren. Hierzu muss lediglich WPF als Browseranwendung laufen. Auch ASP.NET oder andere WEB-Anwendungen kann man auf diesem Weg nahtlos integrieren.
Vielleicht hilft das hier beschriebene Beispiel dem ein oder anderen auf dem Weg seine Anwendung auf eine neue Version zu migrieren.
by wmn
16. November 2010 01:00
Auf der DevCon in Frankfurt (www.devcon.dfpug.de) habe ich wieder mal zahlreiche Vorträge gehalten. Mein Dank geht an alle, die so lange durchgehalten haben und sich teilweise sogar abends noch unsere Fallstudie bis 22:30 Uhr angetan haben! Wer es nochmals braucht bzw. die Unterlagen noch nicht hat, kann die Vortragsunterlagen hier herunterladen. Fragen und Ideen sind natürlich jederzeit gerne willkommen.
by wmn
4. October 2008 20:12
Viele von uns haben ja bereits ihre Anwendung auch mit SQL-Server-Datenbanken am laufen. In diesem Fall hat man aber das Problem bei der Verteilung der Anwendung vor Ort die Datenbankstrukturen aktuell zu halten. Unter FoxPro konnte man dafür ja Stonefield Data Dictionary einsetzen was aber mit SQL-Server nicht mehr funktionert. Ein Ersatz hierfür gibt es aber, welcher einem auch die Bereitstellung von entsprechenden Installationsscripts massiv erleichtert: SQL Packager™ Latest version: 5.5 Packages a database or a database update Script your entire database accurately and quickly Move your database from A to B Compress your database as a .exe, or launch as a Visual Studio project Simplify database deployments and installations – SQL Server 2000, 2005 and 2008 Easy roll-out of database application updates across your client base "SQL Packager is one of the most useful tools I have ever seen. The ability to package the database schema and data of preconfigured lookup tables is great. I have tested it using one of our customer databases and it works fine. The new database has been installed correctly on the new server" Thomas Stensitzki, iComCept GmbH Walk-through View our short screen-by-screen introductory walk-through Demo video Step-by-step demo of SQL Packager in action Standard Edition €245 Packages a database or a database update. Download trial Purchase Get a quote Pro Edition €395 SQL Packager Pro offers access to the command line interface. Download trial Purchase Get a quote With SQL Packager Pro, you can package a database or a database update, as well as automate tasks, thanks to the incorporated command-line interface. SQL Packager works by scripting and compressing the entire database – schema and data together – and packaging it up as a deliverable .exe file. It does this by scripting separate transactions for schema and data respectively. When migrating databases, all schema objects, all new tables and their data as well as data with unique indexes or constraints are also scripted. Installing, updating, or distributing your database now becomes a simple point-and-click procedure. Deploy SQL Server 2005 databases, and applications that depend on them, with no manual scripting! By letting you compress your packaged database, you can reduce storage overhead and distribute it faster and more easily. SQL Packager also makes it easy to archive your database, and is an excellent solution for making a backup of your database where you don't have SQL Server administration rights. You can read how iCOMcept in Aachen, Germany, minimized their application deployment window using SQL Packager.
by wmn
12. September 2008 20:16
Kürzlich habe ich ein Beispiel für den Zugriff auf FoxPro-Tabellen mit LINQ gefunden. Das Beispiel ist leider in VB geschrieben, was sich ja aber leicht umstellen lässt…. Today, we'll make a query to my Foxpro picture database of 28,000 pictures/movies. We'll make the query result structure to be similar to the prior post, so the same code will show the picture or movie in a tooltip. The query is for "wheel", which includes pictures of cartwheels, Ferris Wheels, wheelbarrows. We can use the System.Data.DataRowExtensions to access the DataTable filled by the OledbDataAdapter without using DLINQ Dim Query If True Then ' if you want the prior sample, just change this to false Dim dt As New DataTable _rootFolder = "e:\Pictures" Dim da = New OleDbDataAdapter( _ "Select fullname as FileName,Notes from mypix where 'wheel'$lower(notes)", _ "Provider=VFPOLEDB.1;Data Source=" + _rootFolder + "\mypix.dbf") da.Fill(dt) Query = From picrow In dt.AsEnumerable _ Select FileName = picrow.Field(Of String)("FileName"), _ Description = picrow.Field(Of String)("Notes") _ Select FileName, _ Description, _ Type = IO.Path.GetExtension(FileName).Substring(1), _ Size = (New IO.FileInfo(_rootFolder + IO.Path.DirectorySeparatorChar + FileName)).Length Else Query = From file In IO.Directory.GetFiles(_rootFolder, _ "*.*", IO.SearchOption.AllDirectories) _ Select file, Ext = IO.Path.GetExtension(file).ToLower _ Where Ext.Length > 0 AndAlso ".avi .jpg .mid .mpg .wma .wmv".Contains(Ext) _ Select FileName = file.Substring(_rootFolder.Length + 1), _ Type = Ext.Substring(1), _ Size = (New IO.FileInfo(file)).Length End If Start Visual Studio 2008 Choose File->New Project->Visual Basic->WPF Application Double click the form designer to bring up the Window1.xaml.vb file. Replace the contents with the code below. Change the oledb provider connection string to your data Hit F5 to run the program Imports System.Windows.Controls.Primitives ' for Popup Imports System.Data Imports System.Data.OleDb Imports System.Data.DataTableExtensions Partial Public Class Window1 Dim _rootFolder As String = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded Me.Width = 800 : Me.Height = 800 Dim Query If True Then ' if you want the prior sample, just change this to false Dim dt As New DataTable _rootFolder = "e:\Pictures" Dim da = New OleDbDataAdapter( _ "Select fullname as FileName,Notes from mypix where 'wheel'$lower(notes)", _ "Provider=VFPOLEDB.1;Data Source=" + _rootFolder + "\mypix.dbf") da.Fill(dt) Query = From picrow In dt.AsEnumerable _ Select FileName = picrow.Field(Of String)("FileName"), _ Description = picrow.Field(Of String)("Notes") _ Select FileName, _ Description, _ Type = IO.Path.GetExtension(FileName).Substring(1), _ Size = (New IO.FileInfo(_rootFolder + IO.Path.DirectorySeparatorChar + FileName)).Length Else Query = From file In IO.Directory.GetFiles(_rootFolder, _ "*.*", IO.SearchOption.AllDirectories) _ Select file, Ext = IO.Path.GetExtension(file).ToLower _ Where Ext.Length > 0 AndAlso ".avi .jpg .mid .mpg .wma .wmv".Contains(Ext) _ Select FileName = file.Substring(_rootFolder.Length + 1), _ Type = Ext.Substring(1), _ Size = (New IO.FileInfo(file)).Length End If Me.Content = New BrowseWithMedia(Query, Me) Me.Title = _rootFolder + " #Items = " + _ CType(Me.Content, BrowseWithMedia).Items.Count.ToString End Sub Private Sub Window1_Deactivated(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Deactivated Dim pop = CType(Me.Content, BrowseWithMedia)._lvPopUp ' no popup if user changes active window (Alt-Tab) If Not pop Is Nothing AndAlso pop.IsOpen Then pop.IsOpen = False End If End Sub Private Class BrowseWithMedia : Inherits Browse Dim _Parent As Window1 ' reference to container Friend _lvPopUp As Popup ' popup window to show tip with movie Dim WithEvents _lvTimer As New System.Windows.Threading.DispatcherTimer ' timer: after delay, show tip Sub New(ByVal Query As Object, Optional ByVal Parent As Object = Nothing) MyBase.new(Query, Parent) _Parent = Parent End Sub Sub HandleWheel(ByVal sp As StackPanel, ByVal e As System.Windows.Input.MouseWheelEventArgs) 'Handles Me.MouseWheel Dim transform = CType(sp.RenderTransform, ScaleTransform) Dim melem As MediaElement = sp.Children(1) If e.Delta > 0 Then melem.Height *= 1.1 melem.Width *= 1.1 'transform.ScaleX *= 1.1 ' to scale tip text too 'transform.ScaleY *= 1.1 Else 'transform.ScaleX /= 1.1 'transform.ScaleY /= 1.1 melem.Height /= 1.1 melem.Width /= 1.1 End If End Sub Dim _mdownPt As Point Sub HandleClick(ByVal sp As StackPanel, ByVal e As MouseButtonEventArgs) If sp.IsMouseCaptured Then sp.ReleaseMouseCapture() End If If e.LeftButton = MouseButtonState.Pressed Then _mdownPt = Mouse.GetPosition(sp) ' record mouse pos relative to sp sp.CaptureMouse() End If End Sub Sub HandleMouseMove(ByVal sp As StackPanel, ByVal e As MouseEventArgs) If Mouse.LeftButton = MouseButtonState.Pressed Then If sp.CaptureMouse Then Dim curpt As Point = sp.PointToScreen(Mouse.GetPosition(sp)) _lvPopUp.HorizontalOffset = curpt.X - _mdownPt.X ' subtract mouse pos rel to sp _lvPopUp.VerticalOffset = curpt.Y - _mdownPt.Y _lvPopUp.Placement = PlacementMode.Absolute ' PlacementMode.Mouse End If End If End Sub Sub HandlePopupTimerTick() Handles _lvTimer.Tick ' show the popup _lvTimer.IsEnabled = False ' enabled in HandleRowSelected Dim lbi As ListBoxItem = Me.ItemContainerGenerator.ContainerFromIndex(Me.SelectedIndex) If Not lbi Is Nothing Then Dim sp As New StackPanel Dim cSrcFile = _Parent._rootFolder + IO.Path.DirectorySeparatorChar + lbi.Content.FileName.ToString Dim XAMLPopup = _ <Popup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Placement="Right" IsOpen="False" > <StackPanel Orientation="Vertical"> <StackPanel.RenderTransform> <ScaleTransform ScaleX="1" ScaleY="1"/> </StackPanel.RenderTransform> <TextBlock Foreground="Black" Background="LightYellow"> <%= cSrcFile + " " + CType(lbi.Content.size / 1024, Int32).ToString("n0") + "K" %> </TextBlock> <MediaElement xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="MyVid" Height="250"> <MediaElement.Triggers> <EventTrigger RoutedEvent="MediaElement.Loaded"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <MediaTimeline Source=<%= cSrcFile %> Storyboard.TargetName="MyVid" RepeatBehavior="Forever"/> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </MediaElement.Triggers> </MediaElement> </StackPanel> </Popup> _lvPopUp = System.Windows.Markup.XamlReader.Load(XAMLPopup.CreateReader) _lvPopUp.PlacementTarget = lbi _lvPopUp.IsOpen = True Dim stackpanel As StackPanel = _lvPopUp.Child ' only child is the stackpanel AddHandler stackpanel.MouseWheel, AddressOf HandleWheel ' mouse wheel will zoom in/out AddHandler stackpanel.MouseDown, AddressOf HandleClick ' mouse click/drag will move picture/movie AddHandler stackpanel.MouseUp, AddressOf HandleClick AddHandler stackpanel.MouseMove, AddressOf HandleMouseMove End If End Sub Sub HandleRowSelected(ByVal sender As Object, ByVal e As RoutedEventArgs) Handles Me.SelectionChanged Dim lb = CType(sender, ListBox) If Not lb Is Nothing AndAlso (lb.SelectedIndex >= 0) Then Dim lbi As ListBoxItem = lb.ItemContainerGenerator.ContainerFromIndex(lb.SelectedIndex) If Not _lvPopUp Is Nothing AndAlso _lvPopUp.IsOpen Then _lvPopUp.IsOpen = False End If _lvTimer.Stop() ' stop prior timer, if any _lvTimer.Interval = TimeSpan.FromMilliseconds(500) _lvTimer.Start() End If End Sub End Class End Class Class Browse Inherits ListView Sub New(ByVal Query As Object, Optional ByVal Parent As Object = Nothing) Dim gv As New GridView Me.View = gv Me.ItemsSource = Query If Not Parent Is Nothing Then If Parent.GetType.BaseType Is GetType(Window) Then CType(Parent, Window).Title = "# items = " + Me.Items.Count.ToString End If End If Me.AddHandler(GridViewColumnHeader.ClickEvent, New RoutedEventHandler(AddressOf HandleHeaderClick)) For Each mem In From mbr In _ Query.GetType().GetInterface(GetType(IEnumerable(Of )).FullName) _ .GetGenericArguments()(0).GetMembers _ Where mbr.MemberType = Reflection.MemberTypes.Property Dim coltype = CType(mem, Reflection.PropertyInfo).PropertyType.Name Select Case coltype Case "Int32", "String", "Int64" Dim gvc As New GridViewColumn gvc.Header = mem.Name gv.Columns.Add(gvc) If coltype <> "String" Then gvc.Width = 80 Dim XAMLdt = _ <DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <StackPanel Orientation="Horizontal"> <TextBlock Name="tb" Text=<%= "{Binding Path=" + mem.Name + "}" %> Foreground="Black" FontWeight="Bold" Background="SpringGreen"> </TextBlock> </StackPanel> </DataTemplate> gvc.CellTemplate = System.Windows.Markup.XamlReader.Load(XAMLdt.CreateReader) Else gvc.DisplayMemberBinding = New Binding(mem.Name) gvc.Width = 180 End If End Select Next Dim XAMLlbStyle = _ <Style xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" TargetType="ListBoxItem"> <Setter Property="Foreground" Value="Blue"/> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Foreground" Value="White"/> <Setter Property="Background" Value="Aquamarine"/> </Trigger> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Foreground" Value="Red"/> </Trigger> </Style.Triggers> </Style> Me.ItemContainerStyle = Windows.Markup.XamlReader.Load(XAMLlbStyle.CreateReader) End Sub Dim _Lastdir As System.ComponentModel.ListSortDirection = ComponentModel.ListSortDirection.Ascending Dim _LastHeaderClicked As GridViewColumnHeader = Nothing Sub HandleHeaderClick(ByVal sender As Object, ByVal e As RoutedEventArgs) If e.OriginalSource.GetType Is GetType(GridViewColumnHeader) Then Dim gvh = CType(e.OriginalSource, GridViewColumnHeader) Dim dir As System.ComponentModel.ListSortDirection = ComponentModel.ListSortDirection.Ascending If Not gvh Is Nothing AndAlso Not gvh.Column Is Nothing Then Dim hdr = gvh.Column.Header If gvh Is _LastHeaderClicked Then If _Lastdir = ComponentModel.ListSortDirection.Ascending Then dir = ComponentModel.ListSortDirection.Descending End If End If Sort(hdr, dir) _LastHeaderClicked = gvh _Lastdir = dir End If End If End Sub Sub Sort(ByVal sortby As String, ByVal dir As System.ComponentModel.ListSortDirection) Me.Items.SortDescriptions.Clear() Dim sd = New System.ComponentModel.SortDescription(sortby, dir) Me.Items.SortDescriptions.Add(sd) Me.Items.Refresh() End Sub End Class