Category Archives: RemObjects

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.

Advertisements

DevExpress Gems – The VCL Filter Control

Recently I was tasked with creating a visual query builder for the reporting. The existing query builder was pretty simplistic, and did not allow for grouping conditions. After talking to a few colleagues and looking into some recommended solutions, a consultant and coworker (and close friend) of mine suggested something that is now obvious, but at the time caught me off guard: why not try utilizing the same filter control shown when you customize the filter of the DevExpress VCL Quantum Grid?

Using the filter control from the Quantum Grid component would immediately carry several benefits:

  • We wouldn’t have to purchase an additional control
  • Our query builder for reports would be the same control customers already use to customize our grids
  • Our query builder for reports would visually match the rest of our application

I was optimistic about being able to reuse the filter control, but honestly thought I’d be digging into the source for the existing Quantum Grid filter customization dialog, hoping for some source I could use. Once I sat back down to my development environment, I was delighted (to say the least) to find both TcxFilterControl and TcxDBFilterControl in my tool palette. This was not the first time (nor, I suspect, the last) that I was completely surprised to find some controls in my DevExpress arsenal of which I was previously unaware.

Did these controls allow us to provide a flexible query builder capable of creating complex SQL? You betcha.

Out of the box, TcxFilterControl is meant to be wired up to something like a TcxGrid table view, while the TcxDBFilterControl is meant to be wired up to a TDataSet descendant. While I normally shy away from database-aware controls in the VCL, the TcxDBFilterControl seemed to be more suited for our needs.

To get started using a TcxDBFilterControl, you can simply drop the control on a form and hook it up to a TDataSet descendant. Immediately, the control is capable of creating complex filters, with the field selections in the filter control populated with the field definitions from the TDataSet descendant.

If you are working with a standards compliant SQL engine, using the TcxDBFilterControl to query data requires only a handful of code. Given a simple VCL project with an ADO connection to the Northwind database, a TcxDBFilterControl hooked up to the EmployeeDefsQuery component, and a grid control hooked up to the EmployeeResultsQuery component, the following code is all you need:

procedure TDemoForm.FormCreate(Sender: TObject);
begin
  EmployeeDefsQuery.SQL.Text := 'Select * From Employees Where 0 = 1';
  EmployeeDefsQuery.Open;
end;

procedure TDemoForm.FetchButtonClick(Sender: TObject);
var
  SqlStatement, WhereClause: string;
begin
  EmployeeResultsQuery.Close;
  SqlStatement := 'Select * From Employees';
  WhereClause := EmployeeFilterControl.FilterText;
  if WhereClause <> '' then
    SqlStatement := SqlStatement + ' Where ' + EmployeeFilterControl.FilterText;
  EmployeeResultsQuery.SQL.Text := SqlStatement;
  EmployeeResultsQuery.Open;
  if EmployeesTableView.ItemCount = 0 then
    EmployeesTableView.DataController.CreateAllItems;
end;

Our database engine is not fully SQL compliant, plus there were some niceties that our previous report query builder supported that had to be preserved (such as not requiring the end-user to use the % mask character with LIKE conditions). While crafting custom SQL from the filter control is not trivial, it is entirely possible and we were able to meet all of our requirements with this very useful control.

To create your own SQL from the supplied filter control, you’ll need to parse the TcxFilterControl.Criteria.Root property. The items within the criteria list can either be criteria items or additional criteria lists (TcxFilterCriteriaItem and TcxFilterCriteriaItemList). The item lists have a BoolOperatorKind property, while the criteria items have an Operator property, along with additional properties, that allow you to fully inspect the filter specified in the filter control and create your own SQL statement. While this is outside the scope of this post, feel free to contact me for more details on how to accomplish this.

I love being surprised by existing controls, new to me, that DevExpress has provided in their suites, allowing me to get my job done better and faster without additional investments in time and money. To be honest, when I thought visual query builder, I just did not think DevExpress. To me this is a showcase control (as seen from other vendors), but DevExpress’s implementation provided everything we needed to get our new report query builder going in no time.

A License for Tide

I’ve had some really good communications with Ron Grove concerning tide. After expressing some interest in the project, I sent him the source code for tide and the Northwind client, which prompted some discussion of the licensing I’d be using for the project.

After a couple of nights reading about many different licensing options, and some great advice from Ron, I think I’ve finally settled on using the FreeBSD/Simplified BSD license. This seems like the least restrictive copyleft license that maintains GPL compatibility. I was also considering MS-PL, but this is not GPL compatible. Now, I don’t personally think that’s a huge stopping point, but I figured if I can use a GPL compatible license, why not?

Hopefully this licensing choice will suit all who wish to download, examine, learn from, use, and possible even modify or redistribute tide. I’ll be wrapping up the second part of the customers plugin soon, and hope to have a blog post up about that this weekend.

Thanks to Ron for helping with my licensing questions!

tide.Northwind.Customers – Part 1

We’ll be breaking the work on the Customers plugin into one or more posts. This first post will net us a working plugin for browsing and editing customers, creating new customers, and deleting customers. This is pretty easy to do using tide – we only need to implement a few of the framework’s interfaces.

However, to keep these posts from getting too long or detailed, we’ll stop at editing our customers within the DevExpress GridControl. The application will be fully functional in this respect, but I’m not a huge fan of editing data directly in a grid – I think it’s handy at times, but for the most part is confusing for end users.

In a follow up post on our Customers plugin, we’ll implement a few more interfaces that allow us to get a details window that displays docked to the right of our browse window. We’ll be able to use that screen to edit our customers, with the rest of the flow working similarly to the way it does in this post.

So, let’s get started.

As with our previous plugins, we’ll add a new project to our existing Northwind solution, specifying a Hydra Plugin Module. To our new plugin module project (Northwind.Customers), we’ll add a new Hydra Visual Plugin. This gives us a Control with a visual design surface. To our visual design surface we’ll add a DevExpress RibbonControl and a DevExpress GridControl. We’ll add a tab, group, and button to our Ribbon control – the tab and group will have a “Customers” caption, and the button will display “Browse Customers”.

Then, we’ll use the Add New Datasource wizard in Visual Studio to add a data source for the Customers table in the Northwind database. This is outside of the scope of this post, but is fairly straight forward. Then, we’ll bind our GridControl to a binding source that is associated with a customers table adapter.

And that’s it for the form design – now we’ll write some code (don’t worry – not too much).

Switching to our source code, we’ll add the following to our using clause:

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

The first namespace gives us access to the interfaces we’ll be implementing. The second namespace gives us access to a base Visual Plugin class from which we’ll inherit our current plugin. We’ll call our visual plugin BrowsePlugin, and adjust it’s declaration:

public partial class BrowsePlugin : MasterWindow, IRibbonProvider, IPrintableProvider

The MasterWindow class is declared in tide.Utilities.Plugins, and does some nice things for us, such as automatically saving and restoring DevExpress GridControl customizations, LayoutControl changes, and providing default implementations for IMasterWindow. IMasterWindow allows the controls in the host’s Quick Access Toolbar and Application Menu (items for saving changes, creating new items, deleting items) to interact with our plugin. Implmenting IRibbonProvider allows us to surface our Ribbon in the host. Finally, IPrintableProvider allows us to make controls within our plugin printable and exportable via the host’s Application Menu.

First, we’ll override several of the IMasterWindow methods that are implemented in the MasterWindow class:

public override string ItemName()
{
    return "Customer";
}

public override bool NewItemEnabled()
{
    return true;
}

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

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

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

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

public override bool DeleteItemEnabled()
{
    return customersBindingSource.Position = 0;
}

These very simple methods allow the Quick Access Toolbar and items in the host’s Application Menu to interact with our plugin.

Next, we’ll implement IRibbonProvider:

public DevExpress.XtraBars.Ribbon.RibbonControl RibbonControl()
{
    return ribbonControl1;
}

And now IPrintableProvider:

public DevExpress.XtraPrinting.IPrintable Printable()
{
    return gridControl1;
}

Now, we’ll add the meat of the code for this plugin. We need to add a couple of namespaces for access to our settings and encryption:

using tide.Utilities.Settings;
using tide.Utilities.Encryption;

Then, we’ll add a Click handler for the Browse Customers button on our RibbonControl, and handle it with the following code:

private void barButtonItem1_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e)
{
    BrowseNorthwindCustomers();
    ((IHostApplication)Host).DisplayPlugin(this);
}

private void BrowseNorthwindCustomers()
{
    string connectionString = GetConnectionString();
    customersTableAdapter.Connection.ConnectionString = connectionString;
    customersTableAdapter.Fill(northwindDataSet.Customers);
    customersView.BestFitColumns();
}

private string GetConnectionString()
{
    ApplicationSettings applicationSettings = new ApplicationSettings((IHostApplication)Host);
    SimpleSettings simpleSettings = new SimpleSettings(applicationSettings.SettingsFileName);
    SimpleEncryption.PassPhrase = "northwindpassphrase";

    string connectionString = simpleSettings.GetSetting("Northwind/ConnectionString", "");
    if (connectionString != "")
        connectionString = SimpleEncryption.SimpleDecrypt(connectionString);

    return connectionString;
}

And that’s it! No sweat broken, I hope. As with our previous examples, we are really just implementing some very simple methods and interfaces from the tide framework. Running the application now lets us browse customers, create new customers, make changes to existing customers (with the Save items in the host’s Quick Access Toolbar and Application Menu enabling as necessary), and save those changes. We can also print our customers, and export them to Excel, Acrobat, and several other formats.

tide.Northwind

In our next post, we’ll update this Customers plugin module, disallowing inline editing in the GridControl and creating a new Hydra Visual Plugin that implements IDetailWindow, allowing us to view the details of a selected customer and make changes within that dedicated screen.