Check out this code! (Part II)

Sign in to queue

Description

In my last article, I discussed a reusable library for communicating with Horizon Information Portal libraries.  In this column, find out how to use that library to create an application to run in your system tray to keep on top of your library books and other materials.  No more excuses for overdue fines!
Arian's Blog

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Visual Basic or Visual C# Express Editions
Hardware: None
Download:

Introduction

Part I of this article covered building the reusable class library (DLL) to communicate with libraries running the Horizon Information Portal software.  In addition to the DLL, I threw together a fairly simple application to support searching and showing checked out materials.  Hopefully if your local library uses the Horizon system, you've already had a chance to try some things out.

This article will take that reusable library and create a desktop application to expose more of the available search features, and allow multiple libraries and patrons to be monitored for upcoming and overdue books.  Concepts will include using a notification icon (appearing in the system tray), context menus (right-click), data binding, and serialization.

The source code is available in both C# and VB (the download links are above the introduction).  The code will open in any version of Visual Studio, including the free Express editions.  To get started with the Express editions of Visual Studio, follow this link: Visual Studio 2005 Express Edition.

Startup and Initialization

The API supports the concept of a library (Horizon.Library) and patrons (Horizon.Patron).  Patrons are associated with a library using a HorizonSession object.  In theory this could allow one patron to be associated with more than one library, but in its current implementation, it's not likely to work since credentials are stored in the Patron object.  If someone belongs to several Horizon-based libraries, feel free to make changes to better support this!

The Library also maintains a list of its users with its Users property.  Because of this, if you have a Library object, you also have references to all of the users.  If you maintain a collection of Library objects (assuming you want the ability to manage multiple libraries in your application) you have references to everything that you need.  In this application, I chose a generic List instance holding Library objects, and made it a public property of the OptionsForm.  It's declared as a private variable, then exposed as a property:

Visual Basic

Private librariesValue As New List(Of Library)()

Public Property Libraries() As List(Of Library)
    Get
        Return librariesValue
    End Get
    Set(ByVal value As List(Of Library))
        librariesValue = value
    End Set
End Property

Visual C#

private List<Library> librariesValue = new List<Library>();

public List<Library> Libraries
{
    get { return librariesValue; }
    set
    {
        librariesValue = value;
    }
}

After your user spends time entering library and patron information, it's probably a good idea to save it somewhere!  The easiest way is with XML or binary serialization, both built into the .NET Framework.  By serializing the librariesValue collection object, you have effectively saved state for everything.  Serialization works using the System.Runtime.Serialization namespace, and for binary format the System.Runtime.Serialization.Formatters.Binary namespace.  Deserialization is the process of converting that binary or XML stream of data back into runtime objects.  The Serialize and Deserialize methods handle this.

Visual Basic

Public Sub Serialize(ByVal file As String, ByVal libs As List(Of Library))
    Dim stream As Stream = New FileStream(file, System.IO.FileMode.Create)
    Dim formatter As IFormatter = New BinaryFormatter()
    formatter.Serialize(stream, libs)

    stream.Close()
End Sub

Public Function Deserialize(ByVal file As String) As List(Of Library)
    Try
        Dim stream As Stream = New FileStream(file, System.IO.FileMode.Open)

        Dim formatter As IFormatter = New BinaryFormatter()
        Dim itemsDeserialized As List(Of Library) = DirectCast(formatter.Deserialize(stream), List(Of Library))

        stream.Close()

        Return itemsDeserialized
    Catch
        Return New List(Of Library)()
    End Try
End Function

Visual C#

public void Serialize(string file, List<Library> libs)
{
    Stream stream = new FileStream(file, System.IO.FileMode.Create);
    IFormatter formatter = new BinaryFormatter();
    formatter.Serialize(stream, libs);

    stream.Close();
}

public List<Library> Deserialize(string file)
{
    try
    {
        Stream stream = new FileStream(file, System.IO.FileMode.Open);

        IFormatter formatter = new BinaryFormatter();
        List<Library> itemsDeserialized = (List<Library>)formatter.Deserialize(stream);

        stream.Close();

        return itemsDeserialized;
    }
    catch
    {
        return new List<Library>();
    }
}

Once the library/patron data is restored, the MainForm.UpdateAllPatrons method is invoked to retrieve details about each patron including books out, overdue, and fines owed.  This is done by looping through each library, and for each library looping through the patrons.  A brief summary of this information is then presented as a balloon tooltip from the notification icon.  You can also update this information by clicking Update Patron Information from the notification icon.

Figure 1 - Context menu for the notification icon (in the system tray)

To manage the list of libraries and patrons, use the Configure menu command, or click Show Search Window to perform a search of your library holdings.

Figure 2 - The Search dialog

The Sort, Limit, and Search by fields come from the initialization of the library.  Values are in the SortOptions, SearchLimits, and SearchOptions properties respectively.

Databinding

After initialization, it's time to databind the search fields.  This is also repeated any time the current library is changed in the ComboBox.  Note that this only affects searching.  If you update patron information, it's always for all configured libraries.  When the current item changes for the librariesComboBox control, it calls UpdateSearchFields:

Visual Basic

Private Sub UpdateSearchFields()
    materialTypesComboBox.DataSource = Nothing
    searchTypeComboBox.DataSource = Nothing
    sortComboBox.DataSource = Nothing

    If libraryValue IsNot Nothing Then
        'Data-bind material types
        materialTypesComboBox.DataSource = libraryValue.SearchLimits
        materialTypesComboBox.DisplayMember = "Description"
        materialTypesComboBox.ValueMember = "Key"

        searchTypeComboBox.DataSource = libraryValue.SearchOptions
        searchTypeComboBox.DisplayMember = "Label"
        searchTypeComboBox.ValueMember = "Key"

        sortComboBox.DataSource = libraryValue.SortOptions
        sortComboBox.DisplayMember = "Label"
        sortComboBox.ValueMember = "Key"

        ' Must have one selected
        If libraryValue.SearchOptions.Count > 0 Then
            searchTypeComboBox.SelectedIndex = 0
        End If
    End If
End Sub

Visual C#

private void UpdateSearchFields()
{
    materialTypesComboBox.DataSource = null;
    searchTypeComboBox.DataSource = null;
    sortComboBox.DataSource = null;

    if (lib != null)
    {
        //Data-bind material types
        materialTypesComboBox.DataSource = lib.SearchLimits;
        materialTypesComboBox.DisplayMember = "Description";
        materialTypesComboBox.ValueMember = "Key";

        searchTypeComboBox.DataSource = lib.SearchOptions;
        searchTypeComboBox.DisplayMember = "Label";
        searchTypeComboBox.ValueMember = "Key";

        sortComboBox.DataSource = lib.SortOptions;
        sortComboBox.DisplayMember = "Label";
        sortComboBox.ValueMember = "Key";

        if (lib.SearchOptions.Count > 0)
        {
            searchTypeComboBox.SelectedIndex = 0; // Must have one selected
        }
    }
}

Notice the call to set the data source to null/nothing for the three ComboBox controls.  If you update a collection and want to update a bound control to show the change, you need to first set the data source to null unless you use DataSource objects (which is a very cool way to go all out!).  In this case, it's not really necessary since it's a different collection for each library, but it's not a bad habit to get into.  You won't cause any problems anyway.  If the selected library turns out to be null/nothing (perhaps the last instance was deleted), the controls will be cleared out.

Searching is mostly a matter of setting up the request and invoking the appropriate method.  The SearchButton_Click event handler includes some error handling, so I'll just show the actual body here.  It's all about taking the values from the ComboBox controls.  I should point out that the Aspect property is set based on what I can figure out from trying various requests.  I don't know for sure that is will work with all libraries.  The issue is that the basic default search only accepts a search type (title/author/keyword) without custom sorting or limits (by branch or collection).  The "subtab14" aspect seems to be the value for an "advanced" search.  It seems like per-library customizations may cause this to not always hold true.

Visual Basic

req.Aspect = "basic_search"
req.SearchTerm = searchTermTextBox.Text
req.Index = searchTypeComboBox.SelectedValue.ToString()

If materialTypesComboBox.SelectedValue IsNot Nothing Then
    req.Limit = materialTypesComboBox.SelectedValue.ToString()
    vreq.Aspect = "subtab14"
Else
    req.Limit = ""
End If

If sortComboBox.SelectedValue IsNot Nothing Then
    req.Sort = sortComboBox.SelectedValue.ToString()
    req.Aspect = "subtab14"
Else
    req.Sort = ""
End If

holdingsDataGridView.DataSource = libraryValue.Search(req)

Visual C#

req.Aspect = "basic_search";
req.SearchTerm = searchTermTextBox.Text;
req.Index = searchTypeComboBox.SelectedValue.ToString();

if (materialTypesComboBox.SelectedValue != null)
{
    req.Limit = materialTypesComboBox.SelectedValue.ToString();
    req.Aspect = "subtab14";
}
else
    req.Limit = "";

if (sortComboBox.SelectedValue != null)
{
    req.Sort = sortComboBox.SelectedValue.ToString();
    req.Aspect = "subtab14";
}
else
    req.Sort = "";

holdingsDataGridView.DataSource = lib.Search(req);

When you first launch the application, you won't actually see anything.  By design, the application can be run at startup and just keep an eye on what's checked out.  In my family, we have four accounts configured.  In just a few seconds, it logs into all accounts, reads the information, and presents a nice little popup with how many books are checked out and how much money is owed.  In the Configuration dialog, you can set it to update this every n hours.  This will cause the popup to appear each time.  You can also manually update the information.  If you check the Auto-Renew checkbox, books will renew the day before or day of (depending on when it notices).  Of course, if you've already renewed or owe money, it might not have any effect.

Figure 3 - General settings

Final Note

As mentioned last time, the API was created by examining the XML to figure out inputs and outputs.  Due to the lack of documentation, and the apparent variation that exists between different library's implementation, some features don't work all that well on some libraries.  If you discover something that doesn't quite work right with your library, take a stab at fixing it and drop me a line.  Try to keep the fix general so it continues to work with other libraries.

After creating the initial project, I noticed that SirsiDynix (the creators of Horizon Information Portal) are getting ready to release a new system, Rome, that may or may not be anything like Horizon.  Time will tell!  At any rate, Rome isn't slated for release until the fourth quarter of 2007.  In the meantime, enjoy this!

Next Steps

This application can definitely use some more polish.  Its spartan interface gets the job done, but could be much improved.  Try adding global hotkey support to trigger a search or update.  The search results pane could also stand some work.  The returned fields vary incredibly from one library to the next.  Author and Title are pretty standard, but you can't really count on anything.  One approach would be to actually examine your returned Holding objects.  If you scan the collection of objects and find all of a given property are empty, hide the column.  Dynamically show only columns with values and you get a more relevant display.

Conclusion

There's just something fun about interfacing with remote systems to add new data possibilities to your applications.  Library data may not be as glamorous as commerce data, but it does have its practical side.  Reducing library fines is certainly worthwhile!  What else?  Mashups for searching multiple libraries and mapping where books are or using a tool like Greasemonkey (a Firefox plugin) to add library results to Amazon.com pages would be cool too.  Download Visual Studio Express today and take it from here.


A2rian Kulp is an independent software developer and writer working in the Midwest.  He has been coding since the fifth grade on various platforms, and also enjoys photography, nature, and spending time with his family.  Arian can be reached through his web site at http://www.ariankulp.com.

Tags:

utility, Windows

The Discussion

  • User profile image
    TomB

    In reference to your article:

    Check out this code! (Part II)

    Published 19 April 07 11:10 AM | Coding4Fun

    I have a question about the part where you update the DataSource of the ComboBox to null.

    materialTypesComboBox.DataSource = null;

    How do you get this to work without getting a NullReferenceException? That's what I keep getting when I step through that line in my code. Any ideas?

    Tom

Add Your 2 Cents