Advanced Tutorial

This section will take you through the process of using the Opf3 Framework, step by step. For the tutorial we have created the following tables and relations in a Microsoft Office Access database.

Tutorial_Tables.gif

The tables are part of a very simple "Call Center" database containing entities named USER (Agents) and CALLS.

The database contains also a relation between the two entities. Each user can have multiple calls and each call has one associated user (one-to-many relation). For each user it's also possible to store an image (BLob) and add additional properties, which haven't been specified explicitly in the database (so called DynamicProperties. They are stored to OTHER_PROPERTIES). Such additional properties would be for example "Employee of the Month".

Setting up Opf3

To use the framework in our application we have to create an instance of the ObjectContext Class. For the tutorial we do this in a function called InitializeApplication. The ObjectContext is stored in a session object. This session object is not provided by .NET (don't confuse it wit the ASP.NET session object)! It's a simple custom class that holds information of the current application's instance.

/// <summary>
/// Initializes the application.
/// </summary>
private static void InitializeApplication()
{
    // Create a storage and the ObjectContext.
    AccessStorage storage = new 
        AccessStorage(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=..\..\database.mdb;");
    ObjectContext objectContext = new ObjectContext(storage);
    // Set a concurrency manager to do optimistic locking.
    objectContext.ConcurrencyManager = new Md5ConcurrencyManager();

    // Create a session object and store the ObjectContext in that object.
    Session session = new Session();
    session.ObjectContext = objectContext;

    // Set the session object as current session object.
    Session.Current = session;
}

The code creates a new instance of the AccessStorage class and connects it with an instance of the ObjectContext. An instance of the MD5ConcurrencyManager class is then associated with the context to have done optimistic concurrency checks.

The initialized and fully operable context is then set as ObjectContext of the current session and from now on we can access the ObjectContext by using the Current property of the Session object.

That's all. The framework is now set up!

Creating persistent objects

The next step takes us to create the persistent objects for the tables. In a first step we will create the User persistent object without the relation to the Call persistent object (we do this later in this tutorial).

Creating persistent objects is very easy. You have only to decorate the class with a PersistentAttribute where you specify the name of the entity and each property that is mapped to a field with a FieldAttribute.

Let's check out the code for the User persistent object:

// Persistent object mapped to the USER table.
[Persistent("USER")]
public sealed class User
{
    private long _id = -1;
    private string _username = null;
    private bool _isAdministrator = false;
    private DateTime? _lastLogin = null;
    private Blob _image = new Blob();
    private DynamicPropertiesCollection _dynamicPropertiesCollection = 
        new DynamicPropertiesCollection();
    
    // Property bound to the ID column. This property is also the identifier
    // and autoincrement (AutoNumber).
    [Field("ID", AllowDBNull = false, Identifier = true, AutoNumber = true)]
    public long ID
    {
        get { return _id; }
        private set { _id = value; }
    }

    // Property bound to the USER_NAME column. It is mandatory. AllowDBNull
    // is therefore false.
    [Field("USER_NAME", AllowDBNull = false)]
    public string Username
    {
        get { return _username; }
        set { _username = value; }
    }

    // Property bound to the ADMINISTRATOR column. It is mandatory. AllowDBNull
    // is therefore false.
    [Field("ADMINISTRATOR", AllowDBNull = false)]
    public bool IsAdministrator
    {
        get { return _isAdministrator; }
        set { _isAdministrator = value; }
    }

    // Property bound to the LAST_LOGIN column. It is not mandatory.
    // We use a nullable DateTime here (see .NET 2.0 documentation) to allow
    // the user to set the property null.
    [Field("LAST_LOGIN")]
    public DateTime? LastLogin
    {
        get { return _lastLogin; }
        set { _lastLogin = value; }
    }

    // Property bound to the LAST_LOGIN column. It is not mandatory.
    // The Blob class holds a large binary array.
    [Field("IMAGE")]
    public Blob Image
    {
        get { return _image; }
        set { _image = value; }
    }

    // Property bound to the OTHER_PROPERTIES column. It is not mandatory.
    // DynamicProperties are soft properties, that are stored during runtime
    // as xml in one column of the database table. 
    [Field("OTHER_PROPERTIES")]
    public DynamicPropertiesCollection OtherProperties
    {
        get { return _dynamicPropertiesCollection; }
        set { _dynamicPropertiesCollection = value; }
    }
}

As seen in the example is the persistent class marked with a PersistentAttribute (where the name of the entity is specified) and each property with a FieldAttribute. The FieldAttribute class has properties to specify the behaviour of the storage field.

The FieldAttribute on the ID property says that this property is an Identifier, can't be DBNull (AllowDbNull is false) and uses AutoNumbers to generate the identifier value. Autonumbers are autoincrement numbers that are created by the storage and supported by each storage that ships with the framework (except OleDbStorage).

Since the value of the ID property is generated by the storage the set accessor is private. The framework can still change the value of the property but you can't from your application code.

Our "Call Center" database consists of two entities. We have therefore to create also the Call persistent object. The following code shows the implementation of that persistent object (also without the relation to the User persistent):

// Persistent object mapped to the CALLS table.
[Persistent("CALLS", PoolSize = 10)]
public sealed class Call
{
    private long _id = -1;
    private long? _userId = null;
    private string _callName = null;
    private int _callDuration = 0;

    // Property bound to the ID column. This property is also the identifier
    // and autoincrement (AutoNumber).
    [Field("ID", AllowDBNull = false, Identifier = true, AutoNumber = true)]
    public long ID
    {
        get { return _id; }
        private set { _id = value; }
    }

    // Foreign key property. This property holds the foreign key to the User ID.
    [Field("ID")]
    public long? UserID
    {
        get { return _userId; }
        set { _userId = value; }
    }
    
    // Property bound to the CALL_NAME colum.
    [Field("CALL_NAME")]
    public string CallName
    {
        get { return _callName; }
        set { _callName = value; }
    }

    // Property bound to the CALL_DURATION column.
    [Field("CALL_DURATION")]
    public int CallDuration
    {
        get { return _callDuration; }
        set { _callDuration = value; }
    }
}

The Call persistent object is quite the similar to the User persistent. The properties are different and the PersistentAttribute has the property PoolSize set to a value. By specifiying this property to a value other then 0 the framework creates a pool where empty objects of this type are stored. This allows to load faster large collections.

The two persistent objects are already fully operational: You may load them, store them to the storage etc. It's not requried to write any update, insert, delete queries (they are all done by the framework).

Relationships

As mentioned you can now work with the persistent objects. But the relations of the entities specified in the storage are still missing. Let's add them: The following code shows the User persistent object extended by the relation to the Call objects.

[Persistent("USER")]
public sealed class User
{
    private long _id = -1;
    private string _username = null;
    private bool _isAdministrator = false;
    private DateTime? _lastLogin = null;
    private Blob _image = new Blob();
    private DynamicPropertiesCollection _dynamicPropertiesCollection = 
        new DynamicPropertiesCollection();

    // By setting the RelationAttribute we specify the relation between the
    // User object and the Call objects.
    [Relation("ID = UserID")]
    private ObjectSetHolder<Call> _objectSetHolder = new ObjectSetHolder<Call>();
    
    [Field("ID", AllowDBNull = false, Identifier = true, AutoNumber = true)]
    public long ID
    {
        get { return _id; }
        private set { _id = value; }
    }
    
    // ... All other properties of the User persistent object.

    /// <summary>
    /// Gets and sets the calls associated with this object.
    /// </summary>
    public ObjectSet<Call> Calls
    {
        get { return _objectSetHolder.InnerObject; }
        set { _objectSetHolder.InnerObject = value; }
    }
}

In order to add the relation to the User persistent object you have only to add an instance of the ObjectSetHolder class to the object. The ObjectSetHolder class must be decorated with a RelationAttribute that specifies the relation between the User object and it's Call persistent objects.

The persistent object has also been extended by a Calls property that returns the objects managed by the ObjectSetHolder class.

All associated calls are loaded the first time you access the InnerObject property. This is called delayed loading and reduces the data send over the network. You may also specify conditions as arguments in the constructor of the ObjectSetHolder class, to load for example only associated calls with a duration longer then 10 minutes.

The next step takes us to extend the Call persistent object to return the user who did the call:

[Persistent("CALLS", PoolSize = 10)]
public sealed class Call
{
    private long _id = -1;
    private long? _userId = null;
    private string _callName = null;
    private int _callDuration = 0;
    
    // By setting the RelationAttribute we specify the relation between the
    // Call object and the User object.
    [Relation("UserID = ID", PersistRelationship = PersistRelationships.ChildFirst)]
    private ObjectHolder<User> _objectHolder = new ObjectHolder<User>();
    
    [Field("ID", AllowDBNull = false, Identifier = true, AutoNumber = true)]
    public long ID
    {
        get { return _id; }
        private set { _id = value; }
    }
    
    // ... All other properties of the Call object.

    /// <summary>
    /// Gets and sets the user associated with this object.
    /// </summary>
    public User User
    {
        get { return _objectHolder.InnerObject; }
        set { _objectHolder.InnerObject = value; }
    }
}

The extended Call persistent object does not use an instance of the ObjectSetHolder class but uses an instance of the ObjectHolder class, since each call has only one associated user (this is a one-to-many relation).

Now we have created the persistent objects and relations between the objects. We can now load persistent objects, save them, get all realated objects, let's find out how to do this.

Working with persistent objects

Now we want use the persistent objects created in the previous steps. Let's pack this in a nice GUI. The following screenshot shows the main window:

Tutorial_Screen1.png

The window contains a DataGridView that shows the user found in the database. There is also an add button to add a new user, an edit button to edit the current selected user and a delete button to delete the selected user. The search button allows to search for user.

The program code to have all user found in the database shown in the DataGridView is quite simple:

// Add the columns to the grid view.
SetupGridView();

// Get the ObjectContext of the current session.
ObjectContext context = Session.Current.ObjectContext;

// Get an ObjectSearcher to search for User objects.
ObjectSearcher<User> searcher = context.GetObjectSearcher<User>();

// Find all User in the storage and store them to an ObjectSet.
_user = searcher.FindAll();

// Connect the dataConnector with the GridView and set the
// result of the FindAll operation as datasource for the DataConnector.
dataGridView1.AutoGenerateColumns = false;
dataGridView1.DataSource = dataConnector1;
dataConnector1.DataSource = _user;

The first line customizes the DataGridView (creates the mapping between the properties of the User object and the columns), then the current instance of the ObjectContext is retrieved from the session. To search all user the programm gets an ObjectSearcher and uses it's FindAll method. The result is then bound to the DataGridView.

Next we have to implement the possibility to add and edit user. The following screenshot shows the "Add/Edit User" dialog:

Tutorial_Screen2.png

The dialog allows to specify a name, set the user as administrator, set a picture for the user, add dynamic properties and edit the calls of the user. Changes on the UI are directly stored in the object (to make the sample easier). When clicking on the "OK"-Button all changes are saved to the database. The following sample shows how the changes are saved:

// Get the ObjectContext from the session.
ObjectContext context = Session.Current.ObjectContext;
// Start a transaction to make the operation atomic.
context.StartTransaction();

try
{
    // Save the changes on the user and the related objects.
    context.PersistChanges(_user);
    // Commit the changes.
    context.Commit();
}
catch (Exception ex)
{
    context.Rollback();

    MessageBox.Show(string.Format("A {0} has been thrown by the storage.", ex.GetType()),
        "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

The session object is used to get the current ObjectContext. The code starts then a transaction (atomic operation) on the storage and persists the changes by using the PersistChanges method of the ObjectContext. If no exception is thrown by the storage the transaction is committed. If an exception is thrown the user is informed about it. We do nothing else here to keep the example simple.

When saving the User persistent object all connected calls are also updated (if they have been changed!). The framework uses an in-memory check to find out if an object has been changed while it has been in memory.

Connecting the calls with the DataGridView works like in the main window. You get them from the object (by using the Calls property) and bind them with the Dataconnector object which is then bound to the DataGridView:

// Connect the DataGridView with the DataConnector and 
// the collection.
dataGridView2.AutoGenerateColumns = false;
dataConnector2.DataSource = _user.Calls;
dataGridView2.DataSource = dataConnector2;

Deleting one call from the Call collection of the User persistent object is also quite simple. The Call object has to be marked as deleted using the MarkForDeletion method of the ObjectContext. After having done this the changes are persisted using PersistChanges of the ObjectContext. The following piece of code shows how it works:

Call call = (Call)dataConnector2.Current;
if (call == null)
    return;

// Get the ObjectContext from the session.
ObjectContext context = Session.Current.ObjectContext;

// Start a transaction to make the operation atomic.
context.StartTransaction();

try
{
    // Mark the call for deletion. 
    context.MarkForDeletion(call);
    // Persist the changes. Delete the call.
    context.PersistChanges(call);
    // Commit the changes.
    context.Commit();
}
catch (Exception ex)
{
    context.Rollback();

    MessageBox.Show(string.Format("A {0} has been thrown by the storage.", ex.GetType()),
        "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

Searching for persistent objects

The following screenshot shows the dialog of the tutorial program that searches for User objects. You may specify a part of the user's Username. It's also possible to get only all users that have associated Call objects by checking the appropriate checkbox.

Tutorial_Screen3.png

To implement the search routine we created a class that inherits from ObjectSearcher. The following code shows how this works:

/// <summary>
/// This class inherits from IteamSearcher and provides a specialized searcher
/// for user objects.
/// </summary>
public class UserSearcher : ObjectSearcher<User>
{
    public UserSearcher(ObjectContext context) : base(context)
    {
    }

    /// <summary>
    /// This is a specialized method for our search.
    /// </summary>
    /// <param name="username">Username of the user that is searched for.</param>
    /// <param name="hasCall">True to show only user with calls.</param>
    public ObjectSet FindForSearch(string username, bool hasCall)
    {
        // Create an query and specify as condition the name
        // of the user must be the same as the one given.
        string query = "Username Like {0}";

        // We check also if the user has calls (if required). And show only user with calls.
        if (hasCall)
            query += " And Call[CallDuration > 0]";

        // Sort the result by the Username (ASC).
        query += " SortBy Username Asc";

        // Create the object query and specify the query.
        ObjectQuery oQuery = new ObjectQuery<User>(query, "%" + 
            username.Replace("*", "%") + "%");
        return Context.GetObjectSet<User>(oQuery);
    }
}

The class must have a constructor that takes an instance of the ObjectContext as argument. In the "searcher" you can then use the context by accessing the Context property.

We implemented a method called FindForSearch that is called by the code of the search form to get the required objects. The method creates an OPath expression and adds a condition to that. In OPath you have to use the names of the properties of the persistent object to specify the required conditions. This and other features make OPath storage independent. In the sample we search for objects that's Username property is like the username set in the textbox of the search form.

After that a simple join is applied (if the checkbox on the UI is checked). A simple join allows you to check if there are conntected objects (related objects) that match the condition specified. Since we want only to check if the user has Call objects we check if there is a call longer then 0 minutes.

To sort the result of the query a "SortBy" statement is applied as last item of the OPath expression. The whole OPath string is then set as argument of the ObjectQuery's constructor.

The last line of the code gets the Context that has been set as constructor argument, executes the ObjectQuery and returns the resultset. Using the UserSearcher is very easy - we have only to create a new instance of the class and execute the FindForSearch method. The result is then bound to the DataGridView

// Get the ObjectContext from the session.
ObjectContext context = Session.Current.ObjectContext;

// Create a new instance of our specialized searcher class.
UserSearcher searcher = new UserSearcher(context);
// Execute the query on the searcher.
ObjectSet<User> resultSet = searcher.FindForSearch(textBox1.Text, checkBox1.Checked);

// Set the DataGridView.
dataGridView1.AutoGenerateColumns = false;
dataConnector1.DataSource = resultSet;
dataGridView1.DataSource = dataConnector1;

// Display the amount of found user.
label2.Text = string.Format("{0} item(s) found", resultSet.Count);

The last line displays then the amount of found objects.

Dynamic Properties

What are dynamic properties? Dynamic properties is xml that is used to extend the persistent during runtime. You or even your customer may add additional properties to any persistent object any time you/he wants (also during runtime).

The implementation of the User persistent object contains a property that uses a DynamicPropertiesCollection. This class holds dynamic properties. The tutorial program allows to add to any User object dynamic properties.

if (form.ShowDialog(this) == DialogResult.OK)
{
    if (form.DynamicProperty != null)
    {
        // Add the dynamic property to the container.
        _user.OtherProperties.Add(form.DynamicProperty);
        PopulateListControl();
    }
}

This piece of code above creates a new dialog that allows to add a dynamic property. The property is then added to the collection and saved to the storage the next time you save the persistent to the storage.

The following screenshot shows the dialog that allows to add a dynamic property.

Tutorial_Screen4.png

The code behind the scenes is very simple. The dialog creates a new dynamic property depending on the selected type. Adds the name and value and stores the dynamic property in the property that is then used to add the dynamic property to the container.

if (comboBox1.SelectedIndex == 0)
{
    // Create a new DynamicProperty and set the name.
    DynamicProperty dynamicProperty = new DynamicProperty(typeof(int), textBox2.Text);

    try
    {
        // Try to set the value.
        dynamicProperty.Value = int.Parse(textBox1.Text);
    }
    catch (Exception)
    {
        MessageBox.Show("Not a valid integer!", "Error", MessageBoxButtons.OK, 
            MessageBoxIcon.Error);
        return;
    }
    
    _dynamicProperty = dynamicProperty;
}
else
{
    // Create a new DynamicProperty and set the name.
    DynamicProperty dynamicProperty = new DynamicProperty(typeof(string), textBox2.Text);
    // Set the value.
    dynamicProperty.Value = textBox1.Text;
    _dynamicProperty = dynamicProperty;
}

Last edited Nov 24, 2009 at 3:58 AM by davidbrown, version 2

Comments

No comments yet.