Advanced programming


Navigable business objects are intended for programs with which data records are created, searched for or deleted.

The search functionality is achieved by navigable business objects using storable queries (ACQueryDefinition) that you have already got to know in the database chapter.

You can navigate using the ACAccessNav <T> navigation class .

Basically, everything is already described in these chapters on how to program a navigable business object. However, here is the summary of the steps again:

  1. Generate the " Primary Navigation Query ".
  2. Optional: Compare or change the standard filter and standard sort order.
  3. Generate an ACAccessNav <T> instance that you assign to a private field.
  4. Overwrite the virtual IAccessNav AccessNav  property from the base class ACBSONav and return your ACAccessNav <T> instance.
  5. Define a property provided with ACPropertyList in which you return the navigation list.
  6. Define a selection property with the ACPropertySelected attribute class and optionally a so-called " Current property " with the ACPropertyCurrent attribute class. More on this in the next section.
  7. Define methods for searching, creating, deleting with the predefined method names so that you can execute the commands from the ribbon. Read about this in the next but one section.

 


Your navigable business objects should be represented according to the master-detail pattern , as in the following example:

 

This combination of the fold-out window and the detail area is implemented with the control element VBDockingManager . The docking manager basically shows two designs:

 

Master (Explorer):

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

The SelectedMaterial property specified in VBContent must be defined in your business object:

[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");
}
}

The VBDataGrid implemented by IVBSource can automatically bind the matching data collection  because both have been identified with the same group name  in the ACPropertySelected and ACPropertyList .public IList<Material> MaterialListMaterial.ClassName

The Selected property works internally with the Selected property of the ACAccessNav <T> class . This is necessary so that the navigation buttons in the menu ribbon can be used to navigate from the current position (see next section).

 

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>

A property with the name CurrentMaterial was addressed in the VBContent .

Now you will be asking yourself, "Why not the SelectedMaterial ?". 

The answer is: yes you can of course use the SelectedMaterial. The Current property , which by the way is provided with the attribute class ACPropertyCurrent , is an optional possibility to separate the selected line in an ItemsControl (e.g. Datagrid) from the displayed object in the detail area. For example, if you want to navigate further without changing the detail area. This is a special case , but in this example it is mentioned for the sake of completeness.

However , to keep both the Selected and Current properties in sync , use the AccessPrimary.Current property:

[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 lower levels

In the case of hierarchical data structures, you should continue the master-detail principle. We recommend using the docking mechanism here as well. The list should always be on the left and the details area on the right. 

The following example shows a production order from "iPlus MES". A three-level data hierarchy is shown here, in which the master-detail pattern is applied recursively:

 

Take a look at the BSOInOrder.cs business object from the sample project . A two-level hierarchy is implemented there.

 


 

Commands on the ribbon
Iconmethod

This button is used to search in the database. The VBContent is "! Search" .

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

Calling the NavSearch () method is essential! It executes a query on the database and fills the AccessPrimary.NavList with the query result. It then navigates to the first data record found and implicitly calls the Load () method:

This button is used to update or load a data record or entity objects. The VBContent is "! 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;
}

The Load method is not only called via this button, but every time one of the four navigation methods below has been called so that the current data record (current entity object) is freshly reloaded from the database.

So that you do not have to program this explicitly for each business object, the base class ACBSO provides the following method:

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

This method has three delegates that you use to define how the Selected and Current properties are to be accessed. Since the above example has a Selected and a Current property, the getter call was passed in each case and the setter call to the Current property in the third parameter. If you have only defined one  property  (Current or Selected), then set the first parameter to zero:

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

In the last parameter you pass a LINQ To Entites query to query the currently selected object. Remember to add additional include statements  to avoid lazy loading later  and to improve the performance of your application.

The requery parameter is always false when navigating. When you press the button in the ribbon, it is transferred with  true . Then the query is carried out with the MergeOption "OverwriteChanges" so that already materialized entity objects are updated with fresh data. However, if the database context is in a changed state and the user has refused to save, the query is only made with "AppendOnly" !

This key is used to navigate to the first data record. The VBContent is "AccessPrimary! NavigateFirst" . This method is not a method of ACBSONav, but of the class ACAccessNav <T> . You do not need to program these navigation methods in your business object.

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

Note: All four navigation methods always call the Load () method at the end!

This key is used to navigate to the previous data record. The VBContent is "AccessPrimary! NavigatePrev" .

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

 

This key is used to navigate to the next data record. The VBContent is "AccessPrimary! NavigateNext" .

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

 

This key is used to navigate to the last data record. The VBContent is "AccessPrimary! NavigateLast" .

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

 

This key is used to create a new data record. The VBContent is "! New" . You have to program this method explicitly in your business object according to the following scheme:

[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" is a so-called manager class with which a new, unique secondary key can be generated for each application table .

This key is used to delete a data record. The VBContent is "! Delete" . You have to program this method explicitly in your business object according to the following scheme:

[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 this example, the entity object to be deleted has the DeleteDate property because it implements the IDeleteInfo interface:

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

This interface indicates that an EntityObject does not have to be completely deleted from the database when it is deleted, but can also be archived. 

For this reason, a query dialog (design name: "DialogSoftDelete") should appear first with which the user is asked whether the data record should be archived or deleted. Depending on what the operator clicks, the predefined, virtual method OnDelete () is called. If the operator has decided on archiving , the "softDelete" parameter is true .

If the EntityObject has already been archived, a physical deletion process takes place by calling OnDelete () with false .

So if your Entity class does not offer an archiving option because IDeleteInfo has not been implemented, then always call OnDelete () in your Delete () method with false .

This button is used to restore a data record that was archived. The VBContent is "! Restore" .

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

If your Entity class does not offer an archiving option because IDeleteInfo has not been implemented, then do not declare a Restore () method.

This button is used to export data. The VBContent is "! DataExportDialog" .

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