Blog Schröder

Sammlung von Codeschnipseln zu Programmierproblemen.
Daten aus fremden Quellen unterliegen deren Rechten.
Siehe auch: Disclaimer auf www.computer-schroeder.de

Dienstag, 29. September 2009

Fehler 8155: Keine Spalte wurde für Spalte x von DRVD_TBL angegeben

System: Access 2003 (ADP mit SQL Server 2005)

Dies ist ein Hinweis auf eine fehlerhafte SQL-Anweisung.
Bei mir entstand der Fehler, als die SQL-Anweisung zusammengestückelt wurde:

DoCmd.RunSQL "INSERT INTO myTAB (Wert) VALUES (" & txtFeld & ")"

Alles geht gut, wenn in txtFeld eine ganze Zahl steht. Steht aber eine Kommazahl (z.B. 0,5) drin, dann erscheint in der VALUES-Liste das  Komma, was bedeutet: es werden zwei Werte statt einem übergeben.
Das Komma wird mit folgender Schreibweise korrekt in den SQL-Dezimalpunkt umgewandelt:

DoCmd.RunSQL "INSERT INTO myTAB (Wert) VALUES (" & str(txtFeld) & ")"

Labels: , , , , ,

Dienstag, 23. Juni 2009

In Datenherkunft von Formularen und Berichten Textteile austauschen

Bei Änderungen an der Datenbankstruktur sollen auch Tabellen- und Feldnamen geändert werden. In Modulen geht das einfach durch STRG + H (Ersetzen). Die in Access-Objekten eingebetteten SQL-Anweisungen werden so nicht erwischt. Hierzu folgende Funktion, die zweckmäßigerweise aus einem Makro (s. Abb.) aufgerufen werden kann.

Argumente:

  1. oldString: das alte Textstück (z.B. "Länderschlüssel")
  2. newString: das stattdessen einzusetzende (z.B. "Laenderschluessel")
  3. findInForms: Ersetze in RecordSource jedes Formulars und Berichts (nur FALSE, falls nicht gewünscht)
  4. findInControls: Ersetze in RowSource aller Listen- und Kombinationsfelder (nur FALSE, falls nicht gewünscht)



Public Function RenamePartInRSources(oldString As String, newString As String, _
Optional findInForms As Boolean = True, Optional findInControls As Boolean = True)
'ersetzt in der Datenherkunft von Form/Control ein Wort durch ein anderes
On Error GoTo Er
Debug.Print "Ersetze " & oldString & " durch " & newString
If oldString = "" Then Err.Raise 10001, , "Parameterfehler oldString"
If newString = "" Then Err.Raise 10002, , "Parameterfehler newString"
If Not (findInForms Or findInControls) Then Err.Raise 10003, , "Parameterfehler findIn*"

Dim AO As AccessObject
Dim F As Form
Dim R As Report

Dim o As String
Dim s As String
Dim i As Integer

For Each AO In Application.CurrentProject.AllForms
DoCmd.OpenForm AO.Name, acDesign, , , acFormEdit, acHidden
Set F = Forms(AO.Name)
If findInForms Then 'in Formular ersetzen
o = F.RecordSource
' Debug.Print F.Name & ": " & F.RecordSource
s = Replace(o, oldString, newString)
F.RecordSource = s
If s <> o Then Debug.Print F.Name & ": " & F.RecordSource
s = ""
End If

If findInControls Then 'in Steuerelement ersetzen
For i = 0 To F.Controls.Count - 1
On Error Resume Next
o = F(i).RowSource
' Debug.Print F.Name & "." & F(i).Name & ": " & F(i).RowSource
s = Replace(o, oldString, newString)
F(i).RowSource = s
If s <> o Then Debug.Print F.Name & "." & F(i).Name & ": " & F(i).RowSource
On Error GoTo Er
s = ""
Next i
End If
DoCmd.Close acForm, AO.Name, acSaveYes
Next AO

For Each AO In Application.CurrentProject.AllReports
DoCmd.OpenReport AO.Name, acDesign, , , acHidden
Set R = Reports(AO.Name)
If findInForms Then 'in Bericht ersetzen
o = R.RecordSource
s = Replace(o, oldString, newString)
R.RecordSource = s
If s <> o Then Debug.Print R.Name & ": " & R.RecordSource
s = ""
End If

If findInControls Then 'in Steuerelement ersetzen
For i = 0 To R.Controls.Count - 1
On Error Resume Next
o = R(i).RowSource
s = Replace(o, oldString, newString)
R(i).RowSource = s
If s <> o Then Debug.Print R.Name & "." & R(i).Name & ": " & R(i).RowSource
On Error GoTo Er
s = ""
Next i
End If
DoCmd.Close acReport, AO.Name, acSaveYes
Next AO
Ex:
Set AO = Nothing
Set F = Nothing
Set R = Nothing
Debug.Print "RenamePartInRSources beendet."
Exit Function
Er: MsgBox "Fehler " & Err.Number & " in Tools.RenamePartInRowSource" _
& vbCrLf & Err.Description
Resume Ex
End Function

Labels: , , , , , , , ,

Dienstag, 2. Juni 2009

Access-Formular: beim Datensatz bleiben

Donkarl.com 4.5 empfiehlt die Bookmark-Methode:
Me.Painting = False 'Bildschirmflackern reduzieren
... 'SQL o.a.
Dim v 'Variant
v = Me.Recordset.Bookmark
Me.Requery 'Requery zeigt normal den ersten Datensatz an
Me.Bookmark = v
Me.Painting = True

Die Suchmethode:
Me.Painting = False 'Bildschirmflackern reduzieren

... 'SQL o.a.
'nach Löschbefehl: auf nächste Zeile
'DoCmd.GoToRecord , , acNext
Dim pos As String
pos = txtDatenfeld
Me.Requery 'Das böse Requery
DoCmd.FindRecord pos, , , , , acAll
Me.Painting = True

Dann gibts auch noch Seek
und ev. DoCmd.GotoRecord Next

Labels: , , , ,

Sonntag, 10. Mai 2009

VBA in MS Office bringt "Unerwarteter Fehler"

Die Lösung
auf: http://www.herber.de/forum/archiv/716to720/t716089.htm
von: Christoph Dümmen
Geschrieben am: 07.01.2006 16:05:30

thanks to Tony Jollans in microsoft.public.word.vba.general

This sounds like corruption. If reinstalling Office doesn't help, try
deleting the VBA key in the registry (which is left unchanged by
(re)installation) ...

HKEY_CURRENT_USER\Software\Microsoft\VBA

If you delete this key, a clean copy should be created from built-in
defaults when you open the VBE.

Labels: , , , , , ,

Freitag, 3. April 2009

Feldinhalt aufteilen, z.B. Straße und Hausnummer

Public Function PosHsNrInStrasse(Strasse As String) As Integer
    Dim Zaehler     As Integer
    Dim Laenge      As Integer
    Dim X           As String
    Dim Ergebnis    As String
   
    Laenge = Len(Strasse)
    PosHsNrInStrasse = 0
'von rechts nach links durch Strassennamen gehen
'bis auf die 3 linken Zeichen damit Strassen, die mit Zahl beginnen
'(z.B. 3. Terwestenweg) nicht als Hausnummer erkannt werden
    For Zaehler = Laenge To 3 Step -1
        X = Mid(Strasse, Zaehler, 1)         'aktuell zu prüfendes Zeichen
        If IsNumeric(X) Then                 'prüfen, ob Zeichen eine Zahl ist
            PosHsNrInStrasse = InStr(Strasse, X)            'Position der Zahl
        End If
    Next
End Function

Public Function HsNr(Strasse As String) As String
    Dim pos     As Integer
    Dim Laenge  As Integer
   
    pos = PosHsNrInStrasse(Strasse)
    Laenge = Len(Strasse)
    If pos > 0 Then
        HsNr = Right(Strasse, Laenge - pos + 1)
      Else
        HsNr = ""
    End If
   
End Function

Public Function StrName(Strasse As String) As String
    Dim pos     As Integer
    Dim Laenge  As Integer
   
    pos = PosHsNrInStrasse(Strasse)
    Laenge = Len(Strasse)
    If pos > 0 Then
        StrName = Trim(Left(Strasse, pos - 1))
      Else
        StrName = Strasse
    End If
 End Function


Quelle: Ludger auf  www.Office-Loesung.de

Labels: , , , , ,

Mittwoch, 1. April 2009

Aus VBA Word-Dokument erzeugen und darin Textmarken überschreiben


filesys:       Filesystemobjekt (vorher angelegt)
wdApp:         Word.Application-Objekt (vorher angelegt)
TEMPLATENAME:  Dateiname (inkl. Pfad) einer .DOT-Datei
STRINGCONTROL: Textbox o.a., enthält Name der Textmarke

...
'
existiert die Dokumentvorlage? 
    Dim bEx As Boolean
    On Error GoTo Er
    If Not filesys.FileExists(TEMPLATENAME) Then
        MsgBox "Die Dokumentvorlage " &
TEMPLATENAME & " wurde nicht gefunden."
        GoTo Ex
    End If
    On Error Resume Next
'prüfen, ob das Word-Objekt (noch) existiert
    Err.Clear
    bEx = wdApp.Documents.Count = 0
    If Err.Number = 462 Or Err.Number = 91 Then
        Set wdApp = Nothing
        Set wdApp = New Word.Application
        Resume
    ElseIf Err.Number > 0 Then
        GoTo Er
    End If
'erstelle das neue Dokument   
    On Error GoTo Er
    wdApp.Documents.Add
TEMPLATENAME, False, wdNewBlankDocument, True

'ersetze Textmarke durch einen Text   
    With wdApp.Documents(wdApp.Documents.Count)  'das zuletzt angelegte Dokument
        On Error Resume Next
        .Visible = False
        'Beispiel: Eine Textmarke namens Firma wird überschrieben
        Dim bmString As String: bmString = STRINGCONTROL & ""
        bEx = .Bookmarks.Exists("Firma")
        If bmString & "" > "" And bEx Then
            wdApp.Selection.Goto what:=wdGoToBookmark, Name:="Firma"
            wdApp.Selection.TypeText Text:=bmString
        End If
...
    End With
'Zeige das Dokument an   
    wdApp.Selection.Goto wdGoToLine, wdGoToFirst
    wdApp.Visible = True
    wdApp.Activate
...
'Fehlerbehandlung
Ex: Exit Function
Er: MsgBox "Fehler " & Err.Number & " in NAMEOFPROCEDURE" _
    & vbCrLf & Err.Description
    Resume Ex
End...



nicht vergessen:

SET filesys=nothing
Set wdApp=Nothing


Labels: , , , , , ,

Die ausgewählten Items einer Listbox in String sichern

Public Sub SelectedFromStringToListBox(ByVal gesamtString As String, Liste As ListBox)
    On Error GoTo Er
    Dim v
    Dim i As Integer
    Dim j As Integer
    With Liste
        For Each v In .ItemsSelected
            .Selected(v) = False
        Next v
        v = Split(gesamtString, "; ")
        For i = 0 To UBound(v)
            For j = 0 To .ListCount - 1
                If v(i) = .ItemData(j) Then
                    .Selected(j) = True
                    Exit For
                End If
            Next j
        Next i
    End With
Ex: Exit Sub
Er: MsgBox "Fehler " & Err.Number & " in SelectedFromStringToListBox" _
    & vbCrLf & Err.Description
    Resume Ex
End Sub


Public Function SelectedFromListbox(ByVal Liste As ListBox) As String
    On Error GoTo Er
    Dim s As String
    Dim v
    s = ""
    For Each v In Liste.ItemsSelected
        If s > "" Then s = s & "; "
        s = s & Liste.ItemData(v)
    Next v
Ex:
    SelectedFromListbox = s
    Exit Function
Er: MsgBox "Fehler " & Err.Number & " in SelectedFromListbox" _
    & vbCrLf & Err.Description
    s = ""
    Resume Ex
End Function



Labels: , , ,

Montag, 19. Januar 2009

Unterformular zeigt leeres Rechteck

Das Problem taucht in Accessprojekten (2002/2003) auf Basis SQL Server 2005 auf.
Die Datenherkunft darf in der ORDER BY-Klausel keine Bezeichnungen wie "XY.ID" enthalten. Erlaubt sind in dieser Klausel nur reine unqualifizierte Feldnamen wie "ID".

Hilfen:
Bearbeite die ORDER BY-Klausel für diesen Zweck in der SQL-Box, nicht per Abfrageassistent (er fügt den Tabellennamen bzw. -alias wieder zu).

Weise allen nicht eindeutigen Feldnamen, die in ORDER BY vorkommen, einen Alias zu und schreibe diesen hinter ORDER BY.

Notfalls eine Tabelle tabelle1 durch eine Unterabfrage ersetzen a la
...
FROM (Select ID as XYID, ... From tabelle1) AS XY
...

Labels: , , ,

Dienstag, 16. Dezember 2008

DTPicker zur Datumeingabe

Value kann nur eingegeben werden, wenn CheckBox auf True steht.

Value = Empty für das aktuelle Datum (als Voreinstellungswert?).

DTPicker1.CheckBox=False läßt die doofe Checkbox schließlich verschwinden.
(Achtung, die CheckBox-Eigenschaft kommt nicht in der Auswahlliste vor!)

---------------------------------------------------------
Praktische Anwendung:

a) DTPicker1 an kein Feld der Tabelle binden!
b) in Form.Current:
'Fülle den Picker mit dem Anfangswert
DTPicker1 .CheckBox = True
DTPicker1 = FeldAusDatenbank (oder Date oder was auch immer)
'kein NULL übergeben!
DTPicker1 .CheckBox = False
c) in DTPicker1_Exit:
'hat sich Wert geändert?
If FeldAusDatenbank = DTPicker1 Then Exit Sub
'schreib den neuen Wert zurück
FeldAusDatenbank = DTPicker1(oder was auch immer)
'eventuell nötig: Datensatz speichern
DoCmd.RunCommand acCmdSaveRecord

Labels: , , ,

Montag, 29. September 2008

Drucken mit VBA

Dim view As Long, merk As String

Private Sub btnAnsicht_Click()
'Druckvorschau oder Ausdruck?
  If Me!btnAnsicht Then
    Me!btnAnsicht.Caption = "Ansicht"
    view = acPreview
  Else
    Me!btnAnsicht.Caption = "Drucken"
    view = acNormal
  End If
End Sub

Private Sub btnDruck_Click()
  DoCmd.OpenReport "rptTest", view
End Sub

Private Sub kmbDrucker_AfterUpdate()
'Ein Drucker wurde gewählt
Dim tmp As String
  tmp = Nz(Me!kmbDrucker, "")
  If tmp <> "" Then SetDefaultPrinter tmp
End Sub

Private Sub Form_Close()
'Standarddrucker setzen
  SetDefaultPrinter merk
End Sub

Private Sub Form_Load()
'alle Drucker in Kombifeld übernehmen
  btnAnsicht_Click
  Me!kmbDrucker.RowSource = GetWindowDeviceNames()
  merk = GetDefaultPrinterName()
  Me!kmbDrucker = merk
End Sub

Labels: , ,

Mittwoch, 13. August 2008

Verbindung eines ADP zur Laufzeit festlegen

Folgende VBA-Prozedur weist dem ADP eine neue Verbindung zu:

Public Sub OpenADPConnection(ByVal strUser As String, ByVal strPassword As String)
Const strCONNECTION_STRING As String ="Provider=SQLOLEDB;Data Source=deinServer;Initial Catalog=deineDatenbank"
CurrentProject.OpenConnection strCONNECTION_STRING, strUser, strPassword
If Not CurrentProject.IsConnected Then
MsgBox "Es konnte keine Verbindung hergestellt werden!"
End If
End Sub


Unangenehmer Effekt:
Access versucht, beim Start einer Anwendung die letzte Verbindung wiederaufzubauen und endet nach Wartezeit mit einem Fehler.
Verhinderung:
Man sollte seine ADP-Anwendung ohne Verbindungsdaten ausliefern und dann, wie oben beschrieben, die Verbindung per VBA beim Start der Anwendung aufbauen.
Die gespeicherten Verbindungsinformationen kann man aus der Anwendung entfernen, indem man per VBA die OpenConnection-Methode ohne Parameter aufruft.

Call CurrentProject.OpenConnection


Danach ist das ADP beim Start verbindungslos und es wird nicht mehr automatisch versucht, eine Verbindung aufzubauen.

Labels: , , , , ,

Samstag, 1. März 2008

Systemfarben

-2147483647 Desktop (background)
-2147483636 Hintergrund der Anwendung (appworkspace)

-2147483643 Fenster (window)
-2147483642 Fensterrahmen (windowframe)
-2147483638 Rahmen akt. Fenster (activeborder)
-2147483637 Rahmen eines inaktiven Fensters (inactiveborder)
-2147483640 Fenstertext (windowtext)
-2147483646 Titelleiste akt. Fenster(activecaption)
-2147483645 Titelleiste inakt. Fenster(inactivecaption)
-2147483639 Titelleistentext (captiontext)
-2147483629 Titelleistentext inakt. Fenster (inactivecaptiontext)

-2147483644 Menüleiste (menu)
-2147483641 Menütext (menutext)
-2147483648 Bildlaufleiste (scrollbar)
-2147483635 Hervorheben (highlight)
-2147483634 Text hervorheben (highlighttext)
-2147483631 Abgeblendeter (deaktivierter) Text (graytext)
-2147483625 QuickInfo-Text (infotext)
-2147483624 QuickInfo-Hintergrund (infobackground)

-2147483633 3D-Objekt, Front (buttonface)
-2147483630 Schaltfläche: Text (buttontext)
-2147483628 3D-Objekt, hervorgehoben (buttonhighlight)
-2147483632 3D-Objekt, Schatten (buttonshadow)
-2147483627 3D-Objekt, dunkler Schatten (threeddarkshadow)
-2147483626 3D-Objekt, hell (threedlightshadow)

(office.microsoft.com)

Labels: , , , ,

Montag, 15. Oktober 2007

Access: Dateisystem

Das Beispiel:
Zugriff auf Ordner mit Vorlagen

In Modul (erlaubt zentralen Zugriff):
Public tmplateList 'enthält die Namen der Dateien

Bei Startform (einmalig, da hier der aufwendige Zugriff):
Dim filesys
Dim tmplatePath
Set filesys = CreateObject("Scripting.FileSystemObject")
Set tmplatePath = filesys.GetFolder(_
Application.CurrentProject.Path & templatePath & "\")
Set tmplateList = tmplatePath.files 'enthält die Liste

Eintragen in Combobox (im Combo-Form):
Dim f 'Vollständiger Name mit Pfad
Dim n 'nur der Dateiname ohnePfad und Endung
For Each f In tmplateList
If Right(f, 4) = ".dot" Then
n = Left(f, Len(f) - 4)
n = Right(n, Len(n) - InStrRev(n, "\"))
kmbDateilist.AddItem (f & ";" & n)
End If
Next

Labels: , , , , , ,

Samstag, 13. Oktober 2007

"????????" als Wert eines SQL Server-GUID-Feldes angezeigt

Problembeschreibung
Wenn Sie mit VBA-Code (VBA = Visual Basic für Applikationen) den Wert einer Jet-Replikations-ID oder eines SQL Server-GUID-Feldes (GUID = Globally Unique Identifier) abrufen und anzeigen, erhalten Sie den Wert "????????".
Ursache
Ein GUID ist ein 16-Byte-Datenfeld, das konvertiert werden muss, um von Microsoft Access interpretiert werden zu können.
Lösung
Konvertieren Sie das GUID-Feld mit der Funktion StringFromGUID() in eine Zeichenfolge, damit Access ein GUID-Feld richtig interpretieren und anzeigen kann. Wenn Sie z.B. den folgenden Code auf ein offenes Formular anwenden, in dessen Datensatzherkunft ein GUID-Feld enthalten ist, sehen Sie eine Zeichenfolge hexadezimaler Zahlen in geschweiften Klammern, denen das Wort "guid" vorangestellt ist, wiederum in geschweifte Klammern gesetzt.
Beachten Sie, dass
Debug.Print StringFromGUID(Forms![Formularname]![GUIDFeldname])
etwa so angezeigt wird:
{guid {3B9B63A3-863D-11CF-8CAE-00AA00C0016B}}
aus Microsoft Knowledge Base: 278099 (http://support.microsoft.com/kb/278099/en-us/)

Extraktion und Aufbereitung:

Function °°StringFromGUID(guid As Control) As String
Dim s As String
s = StringFromGUID(guid)
s = Right(s, Len(s) - 6)
°°StringFromGUID = Left(s, Len(s) - 1)
End Function

Labels: , , ,

Mittwoch, 3. Oktober 2007

Telefonwahl mit acCmdAutoDial

Private Sub btnAnwTelNr_Click()
On Error GoTo Er

txtTelNr.SetFocus
DoCmd.RunCommand acCmdAutoDial

Ex: Exit Sub
Er: MsgBox "Fehler " & Err.Number & " in btnAnwTelNr_Click" & vbCrLf & Err.Description
Resume Ex
End Sub

Labels: , , , , ,

Montag, 24. September 2007

GUID in Access erzeugen

Option Explicit

Public Type TYP_GUID
bytes(15) As Byte
End Type

Public Declare Function CoCreateGuid Lib "OLE32.dll" _
(Guid As TYP_GUID) As Long
Public Declare Function StringFromGUID2 Lib "OLE32.dll" _
(Guid As TYP_GUID, _
ByVal lpszString As String, _
ByVal iMax As Long) As Long

Public Function CreateGUID() As String
'// --------------------------------------------------------
'// Methode: | Erzeugen einer eindeutigen GUID
'// --------------------------------------------------------
'// Parameter: | -
'// --------------------------------------------------------
'// Rückgabe: | GUID als String
'// | z.B.:{A7FFA6B1-0377-4A75-8CB9-B95FB5CF545A}
'// --------------------------------------------------------
'// Autor: | Stefan Kulpa
'// | EDV Innovation & Consulting - Dormagen
'// --------------------------------------------------------
Dim uGuid As TYP_GUID
Dim sBuffer As String
Dim lResult As Long

sBuffer = VBA.Space(78)
CoCreateGuid uGuid
lResult = StringFromGUID2(uGuid, sBuffer, Len(sBuffer))
CreateGUID = _
Left$(StrConv(sBuffer, vbFromUnicode), lResult - 1)

End Function
(www.kulpa-online.com)

Labels: ,

Alle Frontends einer Multiuser-DB schließen

Zur Wartung oder zur Datensicherung ist es erforderlich, alle geöffneten Frontends einer Datenbank zu schließen. Oft machen einem die User zusätzliche Arbeit, indem sie nach Feierabend die Datenbank geöffnet lassen.

Dafür habe ich folgende Lösung entworfen. In der Datenbank (Backend) muss eine zusätzliche Tablle (tblShutdown) angelegt werden, die nur das Ja/Nein-Feld shutdown enthält. Diese Tabelle kann man dann in ein Administrationsfrontend einbinden und von dort über das Setzen des Ja/Nein Wertes in dem einzigen Datensatz das globale Schließen der Frontends einleiten.

Im Frontend muss ein Timer laufen (z.B. in einem unsichtbaren Formular), der in gewissen Abständen die Sub closeDB ausführt.

Probleme kann es geben, wenn in einem der Frontends der aktuelle Datensatz eine unvollständige Eingabe ist, die nicht gespeichert werden kann. Diese Situation sollte man unbedingt vor Einsatz der Funktion bedenken.

Sub closeDB()

Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset _
("SELECT shutdown FROM tblShutdown")

If rs!shutdown = True Then
rs.Close
Application.CloseCurrentDatabase
End If

rs.Close

End Sub
(www.codekabinett.com)

Labels: , ,

Verbindung eines ADP zur Laufzeit festlegen

Eigentlich sollte man annehmen, dass man zu diesem Zweck einfach die Connection.Open-Methode der Currentproject.Connection (ADODB.Connection) verwenden kann. Dies trifft aber nur indirekt zu. Es funktioniert nicht, das Connection-Objekt direkt zu verwenden. Stattdessen muss man die OpenConnection- bzw. CloseConnection-Methode des CurrentProject-Objektes verwenden.

Den richtigen ConnectionString für die Verbindung zu ermitteln kann evtl. ein weiteres Problem darstellen. Denn wenn man sich den Connectionstring der CurrentProject.Connection anschaut, wird dort als Provider "Microsoft.Access.OLEDB.10.0" verwendet. Wenn man aber versucht, mit diesem Provider eine Verbindung zu öffnen, schlägt dies immer fehl. Stattdessen muss man auch für Access einen typischen OleDB-ConnectionString zum SQL-Server verwenden und dabei den Provider "SQLOLEDB.1" angeben, so wie man ihn aus der BaseConnection-Property des CurrentProject-Objektes auslesen kann. Es funktioniert übrigens nicht, dort einen Provider für ein anderes DBMS anzugeben.

Mit den obigen Informationen ausgestattet ist es eine Kleinigkeit, eine VBA-Prozedur zu schreiben, die dem ADP eine neue Verbindung zuweist.

Public Sub OpenADPConnection(ByVal strUser As String, ByVal strPassword As String)

Const strCONNECTION_STRING As String = _
"Provider=SQLOLEDB;Data Source=deinServer;Initial Catalog=deineDatenbank;"

CurrentProject.OpenConnection strCONNECTION_STRING, strUser, strPassword

If Not CurrentProject.IsConnected Then
MsgBox "Es konnte keine Verbindung hergestellt werden!"
End If

End Sub

Zuletzt noch ein Hinweis zu einem eng verwandten Problem. Meist möchte man ja die Verbindung eines ADP zur Laufzeit setzen, um eine fertige Anwendung in der Umgebung des Kunden zu deployen. Dabei tritt der unangenehme Effekt auf, dass Access beim Öffnen eines ADP erst mal versucht, die Verbindung zu dem Server und der Datenbank herzustellen, die zuletzt verwendet wurden. Da der Entwicklungsserver i.d.R. bei dem Wechsel von einer Entwicklungsumgebung zu dem Live-System aber nicht mehr direkt zur Verfügung steht, bleibt die Access-Anwendung dann beim Start so lange hängen, bis der Connection-Timeout abgelaufen ist und anschließend wird eine Fehlermeldung ausgegeben, dass der Server nicht erreichbar ist.

Um diesen unangenehmen Effekt zu verhindern, sollte man seine ADP-Anwendung ohne Verbindungsdaten ausliefern und dann, wie oben beschrieben, die Verbindung per VBA beim Start der Anwendung aufbauen. Die gespeicherten Verbindungsinformationen kann man aus der Anwendung entfernen, indem man per VBA die OpenConnection-Methode ohne Parameter aufruft.

Call CurrentProject.OpenConnection

Danach ist das ADP beim Start verbindungslos und es wird nicht mehr automatisch versucht, eine Verbindung aufzubauen.

(www.codekabinett.com)

Labels: , , ,

CurrentUser in Access Data Project (ADP)

In einer Access-MDB-Anwendung war es mit der Funktion CurrentUser() möglich, den angemeldeten Benutzer der Anwendung zu ermitteln. Der Aufruf von CurrentUser liefert in einem ADP aber immer "Admin" zurück, egal wer gerade angemeldet ist.

Die Erklärung dafür ist einfach. In einem ADP wird die Benutzerverwaltung von dem SQL-Server-Backend übernommen. Es gibt in Access tatsächlich nur noch den einen Benutzer "Admin". Wenn man nun wissen möchte, welcher Benutzer am SQL-Server angemeldet ist, muss man dazu auch den SQL-Server befragen.

Die T-SQL-Funktion SUSER_SNAME() liefert den Login des Benutzers am SQL-Server. Dieser Login kann sich durchaus vom Benutzernamen in der aktuellen Datenbank unterscheiden. Dieser Funktion kann die interne ID eines Logins auf dem SQL-Server als Parameter übergeben werden, um den UserName eines anderen Benutzers zu ermittelt. Für unseren Zweck reicht es aber aus, die SUSER_SNAME-Funnktion ohne Parameter aufzurufen. Also sieht das komplette SQL-Statement wie folgt aus:

SELECT SUSER_SNAME()

Wenn man nicht den Login-Namen auf den Server, sondern den Benutzernamen in der aktuellen SQL-Server-Datenbank benötigt, kann man analog zu dem obigen Beispiel die Funktion USER bzw. USER_NAME() verwenden.

Um aus Access heraus diese Information zu erhalten, muss man ein Recordset mit diesem SQL-Statement öffnen und den Wert dieses Feldes auslesen. Diesen Vorgang kann man komfortabel in eine eigene VBA-Funktion kapseln, die dann in dem Projekt die eingebaute Funktion CurrentUser ersetzt.

Hier ein Beispiel für eine solche Funktion:

Public Function AktuellerBenutzer() As String
On Error GoTo AktuellerBenutzer_Err

Dim rs As ADODB.Recordset

Const strSQL As String = "SELECT SUser_SName()"

Set rs = CurrentProject.Connection.Execute(strSQL)

AktuellerBenutzer = rs.Fields(0).Value

AktuellerBenutzer_Exit:
On Error Resume Next
rs.Close
Set rs = Nothing
Exit Function

AktuellerBenutzer_Err:
MsgBox Err.Number & " " & Err.Description, vbExclamation, "Error"
Resume AktuellerBenutzer_Exit
End Function


Die hier erwähnten SQL-Server-Funktionen enstammen dem Funktionsumfang des SQL-Servers 8.0 (SQL 2000).
(www.codekabinett.com)

Labels: , , ,

Datenbank 'xyz' wird schreibgeschützt geöffnet... bei ADP-Mehrbenutzerbetrieb

Wenn mehrere Benutzer die ADP-Datei öffnen, bekommen alle Benutzer nach dem ersten die Meldung 'Die Datenbank 'xyz' wird schreibgeschützt geöffnet...' Was kann man dagegen tun?

Das ist so 'by Design'. Bei einer MDB-Anwendung werden die Informationen, welcher Benutzer welche Objekte bearbeitet und damit sperrt, in der LDB-Datei gespeichert. Dadurch ist es möglich, dass mehrere Benutzer gleichzeitig Schreibzugriff auf ein und dieselbe Datei haben.

Bei einem ADP gibt es keine LDB-Datei, daher muss Access immer davon ausgehen, dass der erste Benutzer, der die Datei öffnet Änderungen an den Objektdefinitionen vornimmt und sperrt die gesamte Datei. Dadurch haben alle weiteren Benutzer nur noch schreibgeschützen Zugriff auf die Datei. Der Schreibschutz gilt nur für die ADP-Datei, d.h. Formulare, Berichte, Module, etc. aber nicht für die Daten auf dem SQL-Server.

Als einfachen Workaround für dieses Problem kann man Access im Runtime-Modus starten und die ADP-Datei öffnen. Im Runtime-Modus kann man sowieso keine Änderungen an den Objektdefinitionen vornehmen, daher unterbleibt in diesem Fall der Hinweis auf dem Schreibschutz.

Diesen Workaround kann man einfach realisieren, indem man die Anwendung über eine Verknüpfung startet und dort den Runtime-Switch in der Befehlszeile angibt. Die Befehlszeile einer Verknüpfung sieht dann etwa wie folgt aus:

"C:\Pfad\zu\MSACCESS.EXE" /runtime "C:\Pfad\zur\Anwendung.adp"

Dieser Workaround funktioniert aber nur mit maximal 20 geöffneten Instanzen der Anwendung. Wenn die Anwendung 21 mal oder mehr geöffnet wird, erscheint die Fehlermeldung 'Die ADP-Datei ist nicht im richtigen Microsoft Access Projektformat.' und es ist nicht mehr möglich weitere Instanzen zu öffnen.

Eine bessere Lösung für das Problem ist es, jedem Benutzer eine eigene Kopie der Datei auf seinem lokalen Rechner zur Verfügung zu stellen. Zu einem bringt dieses Vorgehen leichte Performancevorteile, da die Access-Objekte nicht über das Netzwerk geladen werden müssen. Außerdem ist dieser Ansatz stabiler, da jeder Benutzer nur in seiner eigenen Datei arbeitet und wenn diese beschädigt wird, keine anderen Benutzer beeinträchtig werden.

Um eine Anwendung in einem solchen Szenario regelmäßig updaten zu können, kann auf jedem Client der Start der Anwendung über ein Script oder ein Zusatzprogramm erfolgen, das ggfls. eine aktualisierte Version von einem Netzlaufwerk auf den lokalen Rechner kopiert. Diese Funktionalität kann man entweder selbst entwicklen, oder auf fertige Lösungen wie z.B. den Auto FE Updater von Tony Toews verwenden.
(www.codekabinett.com)

Labels: , , ,

Dienstag, 18. September 2007

Row limit: Nur 10.000 Datensätze

Du hast in einem Access-ADP das Problem, dass du eine große Menge an Datensätzen anzeigen oder verarbeiten (UPDATE/INSERT/DELETE) willst, aber dass immer nur maximal 10.000 Datensätze angezeigt bzw. verarbeitet werden.

Der einfache Grund hierfür ist, dass Access per Standardeinstellung die Anzahl der bearbeiteten Datensätze auf 10.000 begrenzt.

Das Problem lässt sich mit zwei verschiedenen Ansätzen lösen. Die einfache Lösung ist, die Standardeinstellung von Access den eigenen Bedürfnissen anzupassen. Dazu kann man über das Menü Extras-Optionen-Weitere-Client/Server-Einstellungen-Vorgabe der max. Datensätze einfach einen anderen geeigneten Wert einstellen. Diese Vorgabe gilt jeweils nur für die aktuelle Datenbank.

Diese Änderung der Standardeinstellung lässt sich auch über VBA-Code vornehmen, indem man über die SetOption-Methode die Row Limit-Option verändert. Der folgende Beispielcode demonstriert dies (das Row Limit wird auf 500 Datensätze reduziert):

Call Application.SetOption("Row Limit",500)

Bevor man das Thema damit komplett abhakt, sollte man aber bedenken, dass das Access-Team von Mircosoft diese Voreinstellung nicht einfach nur vorgenommen hat, um uns arme Entwickler zu ärgern, sondern dass diese Voreinstellung durchaus ihre Berechtigung hat. Überleg mal; wer will ersthaft eine Liste mit mehr als 10.000 Datensätzen am Bildschirm durchgehen, oder sich gar duch soviele Datensätze in einer Einzelansicht klicken? - Normalerweise niemand! Weiterhin sollte man bedenken, dass mit einer globalen Änderung dieser Option der SQL-Server dazu gezwungen wird eine Menge Datensätze zum Access-Client zu schaufeln, für die sich dort niemand interessiert. Das belastet den Server und das Netzwerk völlig unnötig.

Anstatt das Row Limit für alle Abfragen in den Access-Standardeinstellungen zu verändern kann man auch in alle Stored Procedures auf dem SQL-Server, die Änderungen an dem Datenbestand vornehmen den Rowcount (Synonym für die Row Limit-Option in Access) per T-SQL setzen. Die entsprechende T-SQL-Anweisung lautet einfach:

SET ROWCOUNT 0

Der Wert 0 für den Rowcount deaktiviert die Beschränkung der betroffenen Datensätze vollständig. D.h. es werden immer alle Datensätze verarbeitet, die den jeweiligen Kriterien entsprechen. Wenn man den Rowcount innerhalb einer Stored Procedure setzt, gilt der jeweilige Wert nur innerhalb der Stored Procedure, nicht für die gesamte Verbindung.

Natürlich kann man auch vor AdHoc-SQL-Statements, die man über die Connection des ADPs ausführt, den Rowcount, wie oben beschrieben, per T-SQL auf einen geeigneten Wert setzen. Allerdings möchte ich dieses Vorgehen nicht empfehlen, da man das dann vor jedem einzelnen SQL-Statement berücksichtigen muss und das meiner Meinung nach zu fehlerträchtig ist. Wenn man also von Ad-Hoc-SQL gebrauch macht um Daten zu ändern, sollte man besser dafür sorgen, dass das Row Limit für die ganze Verbindung des ADPs immer korrekt eingestellt ist. Die bessere Alternative ist natürlich grundsätzlich sämtliche DML-Aktionen ausschließlich über Stored Procedures auszuführen.

Ich würde in jedem Fall für alle Stored Procedures, die Daten verändern immer explizit den ROWCOUNT innerhalb der Stored Procedure auf 0 setzen, wie oben beschrieben. Damit ist sichergestellt, dass die Stored Procedure als autarke Einheit immer funktioniert und nur von eventuellen Eingabeparametern, aber nicht von gobalen Einstellungen, wie dem Rowcount der Connection, abhängig ist.

Realisierung:
Private Sub Form_Open(Cancel As Integer)
MaxDatensätze = Application.GetOption("Row Limit")
Application.SetOption "Row Limit", 0
End Sub

Private Sub Form_Close()
Application.SetOption "Row Limit", MaxDatensätze
End Sub

Quelle: www.codekabinett.com

Labels: ,