tide.Northwind.Customers – Part 2

In my previous post on the Customers plugin for the tide Northwind demo, we created an IMasterWindow plugin that displayed the Northwind customers, and allowed for inline editing within DevExpress’ .NET GridControl. This enabled us to create, edit, and delete customers from the Northwind database. However, as I expressed before, I’m not a huge fan of editing within a grid, unless I know the target audience is very savvy. With that in mind, we’ll now create an IDetailWindow plugin that allows us to inspect the details of a row, and make changes within that window, rather than in the grid control itself.

First, let’s look at the key methods within tide that we’ll either be calling or implementing within our plugins:

  • void IHostApplication.DisplayMasterDetails(IMasterWindow sender)
    Called from IMasterWindow plugins to display relevant detail windows.
  • void IHostApplication.DisplayDetailWindow(IMaserWindow parent, IDetailWindow plugin)
    Called from IDetailWindow plugins to display themselves, parented in an IMasterWindow plugin.
  • void IDetailWindow.DisplayDetails(IMasterWindow)
    Implemented by IDetailWindow plugins, to handle displaying details for relevant IMasterWindow plugins.

Let’s get started. We’ll start by adding a new Hydra Visual Plugin to our existing Northwind.Customers Hydra Plugin Module project. Then, we’ll copy the customers binding source from our previously created BrowsePlugin form to our new plugin form, add several DevExpress TextEdits to a LayoutControl instance, and bind them to the customers binding source.

As before, we’ll add a couple of tide namespaces to our using clause:

using tide.PluginInterfaces;
using tide.Utilities.Plugins;

The tide.PluginInterfaces namespace gives us access to the various plugin interfaces that can be implemented. The tide.Utilities.Plugins namespace provides some base classes we can descend from that give us some nice built in functionality. We’ll call this plugin DetailsPlugin and adjust its declaration as shown below:

public partial class DetailsPlugin : DetailWindow

The DetailWindow class, declared in tide.Utilities.Plugins, implements IDetailWindow with default methods. It also descends from BaseWindow, which has some niceties such as automatically persisting DevExpress GridControl changes and LayoutControl customizations.

IDetailWindow works hand-in-hand with IMasterWindow, providing a detail view for the data presented in a class that implements IMasterWindow:

public interface IDetailWindow
{
    void DisplayDetails(IMasterWindow sender);
    bool BeforeSaveDetails(IMasterWindow sender);
    bool AfterSaveDetails(IMasterWindow sender);
    string Caption();
    int Order();
    Image Glyph();
}

Some of IDetailWindow’s methods relate to how the view is presented in the application, such as Caption(), Order(), and Glyph(). Others, such as DisplayDetails(), BeforeSaveDetails(), and AfterSaveDetails(), allow our class to respond to certain events that happen to the IMasterWindow, such as displaying that IMasterWindow’s DetailsObject(), and taking certain actions before or after the IMasterWindow’s DetailsObject() is saved.

We’ll override a few of the DetailWindow methods in our DetailsPlugin, giving us what we need to view the details of a Northwind customer:

public override void DisplayDetails(IMasterWindow sender)
{
    if (sender is BrowsePlugin)
    {
        RemoveControlErrors();

        customersBindingSource.SuspendBinding();
        customersBindingSource.DataSource = sender.DetailsObject();
        customersBindingSource.ResumeBinding();

        (Host as IHostApplication).DisplayDetailWindow(sender, this);
    }
}

public override bool BeforeSaveDetails(IMasterWindow sender)
{
    return dxValidationProvider1.Validate();
}

public override string Caption()
{
    return "Customer Details";
}

public override Image Glyph()
{
    return imageCollection1.Images[0];
}

Our DisplayDetails() implementation checks to see if the calling IMasterWindow is our BrowsePlugin. If so, it simply binds the IMasterWindow’s DetailsObject() to the binding source on our form, and then calls the host’s DisplayDetailWindow() method, which shows the detail window parented in the specified master window.

We also override BeforeSaveDetails() so that we can use a dxValidationProvider to ensure the necessary values are filled in for our customer. The call to RemoveControlErrors() in DisplayDetails() is simply a helper method that removes any existing validation errors when a new object’s details are shown.

Now, we’ll alter the code we wrote before in our BrowsePlugin so that, instead of editing within the grid, our IDetailWindow implementation is displayed instead. We’ll be using a second dataset, editingNorthwindDataSet, as a temporary holding place for the row we are editing. This allows us to make changes in our IDetailWindow plugin without directly affecting the list in the IMasterWindow plugin until the changes are saved.

First, we’ll change our NewItem() method from:

public override void NewItem(object refId)
{
    customersBindingSource.AddNew();
}

to:

public override void NewItem(object refId)
{
    editingCustomersRow = editingNorthwindDataSet.Customers.NewCustomersRow();
    InitializeCustomerRow(editingCustomersRow);

    editingNorthwindDataSet.Customers.Rows.Clear();
    editingNorthwindDataSet.Customers.Rows.Add(editingCustomersRow);

    (Host as IHostApplication).DisplayMasterDetails(this);
}

Here, we create a new row, initialize it with default values, and add it to our editing dataset. Then, we call the host’s DisplayMasterDetails() method, which will iterate through plugins that implement IDetailWindow, calling DisplayDetails() on them, passing in the calling IMasterWindow.

In order for this to work (as seen in our IDetailWindow plugin’s DisplayDetails() implementation above), we need our BrowsePlugin to override DetailsObject(), returning the object that our IDetailWindw plugin is interested in. In this case, it’s our editing row:

public override object DetailsObject()
{
    return editingCustomersRow;
}

Then, we’ll change our implementations of SaveItemEnabled() and SaveItem() from:

public override bool SaveItemEnabled()
{
    return northwindDataSet.HasChanges();
}

public override bool SaveItem()
{
    customersTableAdapter.Update(northwindDataSet.Customers);
    return true;
}

to:

public override bool SaveItemEnabled()
{
    return editingNorthwindDataSet.HasChanges();
}

public override bool SaveItem()
{
    customersTableAdapter.Update(editingNorthwindDataSet);
    northwindDataSet.Customers.LoadDataRow(editingCustomersRow.ItemArray, System.Data.LoadOption.OverwriteChanges);
    return true;
}

Here, we’re simply checking our editing dataset for changes rather than our browsing dataset. With SaveItem(), we save our editing dataset rather than our browse dataset, and we load the added/updated row back into our browsing dataset. Easy enough!

Now, we’ll change our DeleteItem() implementation slightly. Before, since we were editing within the grid, we simple removed the current row in our DeleteItem() implementation:

public override void DeleteItem()
{
    customersBindingSource.RemoveCurrent();
}

These changes would be saved in our previous SaveItem() implementation. However, we’ve now changed the paradigm. DeleteItem() needs to apply the changes, so we’ll make some slight modifications, adding a message dialog to confirm our deletion and then applying the changes:

public override void DeleteItem()
{
    if (MessageBox.Show("Delete the selected customer?", "Delete", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
    {
        customersBindingSource.RemoveCurrent();
        customersTableAdapter.Update(northwindDataSet);
    }
}

The final piece to making this all work is disabling editing in our GridControl by toggling OptionsBehavior.Editable, and then handling the GridControl’s DoubleClick event with the following code:

private void customersView_DoubleClick(object sender, EventArgs e)
{
    NorthwindDataSet.CustomersRow customersRow = (NorthwindDataSet.CustomersRow)northwindDataSet.Customers.Rows[customersBindingSource.Position];

    editingNorthwindDataSet.Customers.Rows.Clear();
    editingNorthwindDataSet.Customers.ImportRow(customersRow);
    editingCustomersRow = (NorthwindDataSet.CustomersRow)editingNorthwindDataSet.Customers.Rows[0];

    (Host as IHostApplication).DisplayMasterDetails(this);
}

Similar to our NewItem() implementation, we’re mostly just juggling dataset rows. We take the current customer row in our browsing dataset and import it into our editing dataset. We save a reference to our editing row (as it is used by our DetailsObject() implementation), and then call the host’s DisplayMasterDetails() method (as in our NewItem() implementation). As stated above, this will display our IDetailWindow plugin, parented in our IMasterWindow plugin.

And that’s it! You can now create and edit customers within a dedicated detail window:

Widgets and Gadgets Northwind Client (4)

You can have as many IDetailWindow implementations as you like. For each one that calls IHostApplication’s DisplayMasterDetails method for a given IMasterWindow, tabs will be created for displaying each IDetailWindow plugin (that’s where the Order() method comes into play). You can also have completely separate Hydra Plugin Modules with IDetailWindow plugins that parent in IMasterWindow plugins from other modules. It’s all fairly loosely coupled, while allowing for a nice degree of integration between plugins.

In the next post, we’ll get into creating a plugin for Northwind Orders, and show how to do things such as showing customer details from an order’s details or creating a new order from a selected customer’s details.

0 thoughts on “tide.Northwind.Customers – Part 2

  1. Pingback: tide.Northwind.Customers – Part 1 « Development Technobabble

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>