Erweiterte Programmierung


Navigierbare Geschäftsobjekte sind für Programme gedacht mit denen Datensätze angelegt, gesucht oder gelöscht werden.

Die Suchfunktionalität bewerkstelligen navigierbare Geschäftsobjekte mittels speicherbaren Abfragen (ACQueryDefinition) die Sie bereits im Datenbank-Kapitel kennengelernt haben.

Die Navigation bewerkstelligen sie mittels der Navigationsklasse ACAccessNav<T>.

Im Grunde genommen ist in diesen Kapiteln bereits alles beschrieben wie Sie ein navigierbares Geschäftsobjekt programmieren. Hier jedoch nochmal die Zusammenfassung der Schritte:

  1. Generieren Sie die "Primary-Navigationquery".
  2. Optional: Vergleichen oder Ändern Sie den Standard-Filter und Standard-Sortierreihenfolge.
  3. Generieren Sie eine ACAccessNav<T>-Instanz die Sie einem privaten Feld zuweisen.
  4. Überschreiben Sie die virtuelle IAccessNav AccessNav Eigenschaft aus der Basisklasse ACBSONav und geben Ihre ACAccessNav<T>-Instanz zurück.
  5. Definieren Sie eine mit ACPropertyList versehene Eigenschaft in der Sie die Navigationsliste zurückgeben.
  6. Definieren Sie eine Selektions-Eigenschaft mit der Attributklasse ACPropertySelected und optionale eine sogenannte "Current-Eigenschaft" mit der Attributklasse ACPropertyCurrent. Mehr dazu im nächsten Abschnitt.
  7. Definieren Sie Methoden zum Suchen, Anlegen, Löschen mit den vordefinierten Methodennamen damit sie die Befehle vom Menüband aus ausführen können. Lesen Sie dazu den übernächsten Abschnitt.

 


Ihre Navigierbare Geschäftsobjekte sollten nach dem Master-Detail-Pattern dargestellt werden wie im folgenden Beispiel:

 

Diese Kombination aus aufklappbarem Fenster und dem Detailbereich wird mit dem Steuerelement VBDockingManager realisiert. Der Dockingmanager zeigt im Grunde genommen zwei Designs an:

 

Master (Explorer):

 <vb:VBDataGrid VBContent="SelectedMaterial" DisabledModes="Disabled">
    <DataGrid.Columns>
        <vb:VBDataGridTextColumn VBContent="MaterialNo" />
        <vb:VBDataGridTextColumn VBContent="MaterialName1" />
    </DataGrid.Columns>
</vb:VBDataGrid>

Die in VBContent angegebene Eigenschaft SelectedMaterial muss in Ihrem Geschäftsobjekt definiert werden:

[ACPropertySelected(302, Material.ClassName, "en{'Material'}de{'Material'}")]
public Material SelectedMaterial
{
get
{
if (AccessPrimary == null)
return null;
return AccessPrimary.Selected;
}
set
{
if (AccessPrimary == null)
return;
AccessPrimary.Selected = value;
OnPropertyChanged("SelectedMaterial");
}
}

Das VBDataGrid, das IVBSource implementiert, kann die dazu passende Datensammlung public IList<Material> MaterialList automatisch binden, weil beide mit demselben Gruppennamen Material.ClassName in der ACPropertySelected und ACPropertyList gekennzeichnet worden sind.

Die Selected-Eigenschaft arbeitet intern mit der Selected-Eigenschaft der ACAccessNav<T>-Klasse. Dies ist notwendig, damit über die Navigationstasten im Menüband von der aktuellen Position aus weiter navigiert werden kann (siehe nächsten Abschnitt).

 

Detail:

 <vb:VBGrid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition MaxWidth="600"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition Height="30"/>
    </Grid.RowDefinitions>
    <vb:VBTextBox Grid.Column="0" Grid.Row="0" VBContent="CurrentMaterial\MaterialNo" />
    <vb:VBTextBox Grid.Column="0" Grid.Row="1" VBContent="CurrentMaterial\MaterialName1" />
</vb:VBGrid>

Im VBContent wurde eine Eigenschaft mit dem Namen CurrentMaterial adressiert.

Nun werden Sie sich fragen: "Warum nicht die SelectedMaterial?". 

Die Antwort ist: Ja Sie können natürlich die SelectedMaterial verwenden. Die Current-Eigenschaft, die übrigens mit der Attributklasse ACPropertyCurrent versehen wird, ist eine optionale Möglichkeit die selektierte Zeile in einem ItemsControl (z.B. Datagrid) zu trennen von dem angezeigten Objekt im Detailbereich. Beispielsweise, wenn man weiter navigieren möchte, ohne den Detailbereich zu ändern. Dies ist ein spezieller Sonderfall, aber in diesem Beispiel soll das der Vollständigkeit halber erwähnt werden.

Damit jedoch beide die Selected-Eigenschaft und Current-Eigenschaft synchron gehalten werden, verwenden sie die "AccessPrimary.Current"-Eigenschaft:

[ACPropertyCurrent(301, Material.ClassName, "en{'Material'}de{'Material'}")]
public Material CurrentMaterial
{
get
{
if (AccessPrimary == null)
return null;
return AccessPrimary.Current;
}
set
{
if (AccessPrimary == null)
return;
AccessPrimary.Current= value;
OnPropertyChanged("CurrentMaterial");
}
}

 

Master-Detail in untergeordneten Ebenen

Bei Hierarchischen Datenstrukturen sollten sie das Master-Detail-Prinzip weiterführen. Wir empfehlen den Dockingmechanismus auch hier zu verwenden. Dabei sollte die Liste immer links sein und auf der rechten Hälfte der Detailbereich. 

Das folgende Beispiel zeigt einen Produktionsauftrag aus "iPlus MES". Hier ist eine dreistufige Datenhierarchie abgebildet, bei der das Master-Detail-Pattern rekursiv angewendet ist:

 

Schauen Sie sich das Geschäftsobjekt BSOInOrder.cs aus dem Beispielprojekt an. Dort ist eine zweistufige Hierarchie implementiert.

 


 

Befehle im Menüband
IconMethode

Diese Taste dient zum Suchen in der Datenbank. Der VBContent ist "!Search".

[ACMethodCommand(InOrder.ClassName, "en{'Search'}de{'Suchen'}", (short)MISort.Search)]
public void Search()
{
if (AccessPrimary == null)
return;
AccessPrimary.NavSearch(DatabaseApp);
OnPropertyChanged("InOrderList");
}

Der Aufruf der NavSearch()-Methode ist zwingend erforderlich! Sie führt eine Abfrage auf der Datenbank durch und füllt die AccessPrimary.NavList mit dem Abfrageresultat. Anschliessend navigiert es zum ersten gefundenen Datensatz und ruft implizit die Load()-Methode auf:

Diese Taste dient zum Aktualisieren bzw. Laden eines Datensatzes bzw. von Entity-Objekten. Der VBContent ist "!Load".

[ACMethodInteraction(InOrder.ClassName, "en{'Load'}de{'Laden'}", 
(short)MISort.Load, false, "SelectedInOrder", Global.ACKinds.MSMethodPrePost)]
public void Load(bool requery = false)
{
if (!PreExecute("Load"))
return;
LoadEntity<InOrder>(requery, () => SelectedInOrder,
() => CurrentInOrder, c => CurrentInOrder = c,
DatabaseApp.InOrder
.Include(c => c.InOrderPos_InOrder)
.Where(c => c.InOrderID == SelectedInOrder.InOrderID));
PostExecute("Load");
}
public bool IsEnabledLoad()
{
return SelectedInOrder != null;
}

Die Load-Methode wird nicht nur über diese Taste aufgerufen, sondern jedesmal wenn eines der vier untenstehenden Navigationsmethoden aufgerufen wurde, damit der aktuelle Datensatz (aktuelles Entityobjekt) frisch von der Datenbank nachgeladen wird.

Damit Sie dies nicht für jedes Geschäftsobjekt explizit programmieren müssen, stellt die Basisklasse ACBSO folgende Methode zur Verfügung:

public virtual void LoadEntity<TEntity>(bool requery, 
Func<TEntity> selectedGetter, Func<TEntity> currentGetter,
Action<TEntity> currentSetter, IQueryable<TEntity> query)
where TEntity : class

Diese Methode besitzt drei Delegaten, mit denen Sie definieren wie auf die Selected- und Current-Eigenschaft zugegriffen werden soll. Da das obige Beispiel eine Selected- als auch eine Current-Eigenschaft besitzt wurde jeweils der getter-Aufruf übergeben und im dritten Parameter der setter-Aufruf auf die Current-Eigenschaft. Falls Sie nur eine Eigenschaft (Current oder Selected) definiert haben, dann setzen Sie den ersten Parameter auf null:

LoadEntity<InOrder>(requery, null, () => SelectedInOrder, c => SelectedInOrder= c, ...)

Im letzten Parameter übergeben Sie eine LINQ-To-Entites-Query zur Abfrage des aktuell selektierten Objektes. Denken Sie daran zusätzliche Include-Anweisungen einzufügen, um späteres Lazy-Loading zu vermeiden und die Performance Ihrer Anwendung zu verbessern.

Der requery-Parameter ist beim Navigieren immer false. Beim Drücken der Taste im Menüband wird er mit true übergeben. Dann wird die Abfrage mit der MergeOption "OverwriteChanges" durchgeführt, damit bereits materialisierte Entity-Objekte mit frischen Daten aktualisiert werden. Befindet sich aber der Datenbankkontext in einem geänderten Zustand und der Benutzer hat eine Speicherung abgelehnt, erfolgt die Abfrage nur mit "AppendOnly"!

Diese Taste dient zur Navigation zum ersten Datensatz. Der VBContent ist "AccessPrimary!NavigateFirst". Diese Methode, ist keine Methode von ACBSONav, sondern von der Klasse ACAccessNav<T>. Sie müssen daher diese Navigationsmethoden nicht in Ihrem Geschäftsobjekt programmieren.

[ACMethodCommand("Navigation", "en{'First'}de{'Erster'}", (short)MISort.NavigateFirst)]
public void NavigateFirst()

Hinweis: Alle vier Navigationsmethoden rufen am Ende immer die Load()-Methode auf!

Diese Taste dient zur Navigation zum vorigen Datensatz. Der VBContent ist "AccessPrimary!NavigatePrev".

[ACMethodCommand("Navigation", "en{'Previous'}de{'Vorheriger'}", (short)MISort.NavigatePrev)]
public void NavigatePrev()

 

Diese Taste dient zur Navigation zum nächsten Datensatz. Der VBContent ist "AccessPrimary!NavigateNext".

[ACMethodCommand("Navigation", "en{'Next'}de{'Nächster'}", (short)MISort.NavigateNext)]
public void NavigateNext()

 

Diese Taste dient zur Navigation zum letzten Datensatz. Der VBContent ist "AccessPrimary!NavigateLast".

[ACMethodCommand("Navigation", "en{'Last'}de{'Letzter'}", (short)MISort.NavigateLast)]
public void NavigateLast()

 

Diese Taste dient zum Anlegen eines neuen Datensatzes. Der VBContent ist "!New". Diese Methode müssen Sie explizit in Ihrem Geschäftsobjekt nach folgendem Schema programmieren:

[ACMethodInteraction(InOrder.ClassName, "en{'New'}de{'Neu'}", (short)MISort.New, 
true, "SelectedInOrder", Global.ACKinds.MSMethodPrePost)]
public void New()
{
string secondaryKey = Root.NoManager.GetNewNo(Database, typeof(InOrder),
InOrder.NoColumnName, InOrder.FormatNewNo, this);
CurrentInOrder = InOrder.NewACObject(DatabaseApp, null, secondaryKey);
DatabaseApp.InOrder.AddObject(CurrentInOrder);
SelectedInOrder = CurrentInOrder;
if (AccessPrimary != null)
AccessPrimary.NavList.Add(CurrentInOrder);
}
public bool IsEnabledNew()
{
return true;
}

"Root.NoManger" ist eine sogenannte Manager-Klasse mit der für jede Anwendungstabelle ein neuer eindeutiger Sekundärschlüssel generiert werden kann.

Diese Taste dient zum Löschen eines Datensatzes. Der VBContent ist "!Delete". Diese Methode müssen Sie explizit in Ihrem Geschäftsobjekt nach folgendem Schema programmieren:

[ACMethodInteraction(InOrder.ClassName, "en{'Delete'}de{'Löschen'}", (short)MISort.Delete, true, "CurrentInOrder", Global.ACKinds.MSMethodPrePost)]
public void Delete()
{
if (!PreExecute("Delete") || !IsEnabledDelete())
return;
if (CurrentInOrder.DeleteDate != null)
ShowDialog(this, ACBSONav.CDialogSoftDelete);
else
OnDelete(true);
PostExecute("Delete");
}
public override void OnDelete(bool softDelete)
{
Msg msg = CurrentInOrder.DeleteACObject(DatabaseApp, true, softDelete);
if (msg != null)
{
Messages.Msg(msg);
return;
}
if (AccessPrimary == null)
return;
AccessPrimary.NavList.Remove(CurrentInOrder);
SelectedInOrder = AccessPrimary.NavList.FirstOrDefault();
Load();
}
public bool IsEnabledDelete()
{
return CurrentInOrder != null;
}

In diesem Beispiel besitzt das zu löschende Entity-Objekt die Eigenschaft DeleteDate weil es die Schnittstelle IDeleteInfo implementiert:

public interface IDeleteInfo
{
string DeleteName { get; set; }
DateTime? DeleteDate { get; set; }
}

Diese Schnittstelle kennzeichnet, dass ein EntityObjekt beim Löschen nicht vollständig aus der Datenbank gelöscht werden muss, sondern auch archiviert werden kann. 

Aus diesem Grund soll zuerst ein Abfragedialog (Designname: "DialogSoftDelete") erscheinen mit dem der Anwender gefragt wird, ob der Datensatz archiviert oder gelöscht werden soll. Je nachdem was der Bediener anklickt wird die vordefinierte, virtuelle Methode OnDelete() aufgerufen. Hat der Bediener sich für eine Archivierung entschieden, ist der "softDelete"-Parameter true.

Falls das EntityObjekt bereits archiviert ist dann erfolgt ein physikalischer Löschvorgang, indem OnDelete() mit false aufgerufen wird.

Wenn also Ihre Entity-Klasse keine Archivierungsmöglichkeit bietet, indem IDeleteInfo nicht implementiert wurde, dann rufen Sie in Ihrer Delete()-Methode OnDelete() immer mit false auf.

Diese Taste dient zum Wiederherstellen eines Datensatzes der Archiviert war. Der VBContent ist "!Restore".

[ACMethodCommand("Restore", "en{'Restore'}de{'Wiederherstellen'}", (short)MISort.Restore, true)]
public void Restore()
{
OnRestore();
}

Wenn Ihre Entity-Klasse keine Archivierungsmöglichkeit bietet, indem IDeleteInfo nicht implementiert wurde, dann deklarieren Sie keine Restore()-Methode.

Diese Taste dient zum Exportieren von Daten. Der VBContent ist "!DataExportDialog".

[ACMethodCommand("Query", "en{'Export'}de{'Export'}", (short)MISort.QueryDesignDlg, true)]
public virtual void DataExportDialog()