Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Searching the Desktop

Searching for a file across a hard drive is a slow, tedious operation. Learn how to take advantage of the Windows Desktop Search API and database to find files very quickly. Add innovative new features to your applications using the search capabilities built-in to Vista and available for Windows XP.
Arian's Blog

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

Introduction

Windows Vista is almost here! One of the things that I've been really excited is the improved searching. Windows Desktop Search (WDS) 3.0 is a great service indexing documents, media files, mail messages, contacts, and more. Even better, it's available for both Windows Vista and Windows XP.

For this article, I'm creating a WDS rapid search tool. It's a proof-of-concept more than anything, but hopefully can serve as a launching point for writing your own search-enabled applications.

All code highlighted in the article is available for download in both C# and Visual Basic on the same page. If you don't have Visual Studio, don't worry! Just download a Visual Studio Express edition (for free) from Microsoft. The sample will run on either XP or Vista, but on XP it will require the WDS 3.0 installation, available as a stand-alone download.

Working with Desktop Search

I have a prediction. If I'm wrong, it's not because it's a bad idea! I see traditional file browse dialogs going away. Windows Desktop Search makes the concept of folders somewhat silly. By populating metadata tags (author, subject, keywords, etc.) even the file name isn't quite as important. A better way to browse for a file is to search relevant tags across the file system. Similar to the way that iTunes provides a multi-list search paradigm, with each list representing a tag (such as genre and artist), a file requestor could use metadata to show only relevant files, regardless of where they are actually saved. I'd love to see this, but is hasn't happened yet.

As a jumpstart to adding searching to your application, I've created a simple application to perform searches as you type, similar to the Start Menu in Windows Vista. In fact, it's only a few steps from being able to perform the same function. If you performed a Process.Start on the filename, you would have your own launcher.

Figure 1 - Searching for "win" using the sample application

Figure 1 - Searching for "win"

Searching the WDS index can be performed one of two ways. There is a COM interface, but even easier (to me anyway) is the OLE database provider interface. If you know the basics of database querying, you can search your file system. The SQL interface is very powerful, providing a wealth of search predicates to really home in on the relevant results. In order to search, you'll need to reference the System.Data and System.Data.OleDb namespaces.

C# Code

using System.Data;
using System.Data.OleDb;

Visual Basic

Imports System.Data 
Imports System.Data.OleDb 

This gives you access to the OleDbConnection, OleDbCommand, and OleDbDataReader classes. If you've queried SQL Server, Oracle, or other databases, you'll feel right at home with this interface. The key is in getting the right connection string, including the right provider.

C# Code

OleDbConnection conn = New OleDbConnection("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';");
conn.Open();

Visual Basic

conn = New OleDbConnection("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
conn.Open()

With a connection, you can now issue queries and work with the results. You can obtain a list of columns from the list of shell properties. Note that not all columns work with the OLE interface. For more information regarding the properties that work, check out my blog entry here. Note that if you get an exception upon creating the connection, this probably means that Windows Desktop Search isn't installed. This won't happen on Vista, but remember that it's a separate install on XP.

SQL queries must include at least two sections: the list of columns, that is, the SELECT clause, and the table or data source, the FROM clause. For the search index, the SELECT clause is comprised of the property names in the above links. The FROM clause is always the same for searches on the local machine: systemindex. As with SQL Server, you can prefix a machine name for searches across a network, machinename..systemindex, though this requires extra setup as it is not enabled by default. Thus, creating a full query may look like this:

 SQL

SELECT FileName, System.Size FROM systemindex 

Of course, this will be a large result set if you don't restrict the results... unless you include a WHERE clause. For instance, find large files with “WHERE System.Size > 10000000”. The sample application is intended to locate files based on entering part of the filename. We could perform a LIKE match on the FileName field, but the CONTAINS predicate seems to provide better performance. Also, instead of using the FileName column, use System.ItemNameDisplay instead. For files, it's the same thing, but ItemNameDisplay is more applicable if you want to include non-files such as contacts or emails:

C#

public static List<string> PerformSearch(string data)
{
    List<string> items = new List<string>();

    OleDbCommand cmd = new OleDbCommand();
    cmd.Connection = conn;
    cmd.CommandText = string.Format(
        "SELECT System.ItemNameDisplay FROM systemindex WHERE CONTAINS(\"System.ItemNameDisplay\", '\"*{0}*\"')",
        data);

    OleDbDataReader results = cmd.ExecuteReader();

    while (results.Read())
    {
        items.Add(results.GetString(0));
    }

    results.Close();

    return items;
}

Visual Basic

Public Shared Function PerformSearch(ByVal data As String) As List(Of String) 
  Dim items As List(Of String) = New List(Of String) 
  Dim cmd As OleDbCommand = New OleDbCommand 

  cmd.Connection = conn 

  cmd.CommandText = String.Format("SELECT System.ItemNameDisplay FROM systemindex WHERE CONTAINS(""System.ItemNameDisplay"", '""*{0}*""')", data) 

  Dim results As OleDbDataReader = cmd.ExecuteReader 

  While results.Read 
    items.Add(results.GetString(0)) 
  End While 

  results.Close() 
  Return items 
End Function 

Each time the text box is updated, the new search string is passed to a background thread to perform the searching. The search could have been performed on the UI thread (right in the event handler) much easier, but it would risk freezing up the user interface or at least reducing its responsiveness if results didn't come back fast enough. The PerformSearch method simply performs the search and returns the collection of filenames. It has no interaction with the user interface whatsoever.

Once the user has selected a file from the list, additional details are retrieved. This is a slightly convoluted process in order to only search for properties relevant to the given file type. I maintain several constant string values with search properties based on file type.  This could clearly use an overhaul.  It's the starting of a database already, and not easily expandable! 

C#

private const string GENERAL_COLUMNS = "\"System.Kind\", \"System.Title\", \"System.ItemFolderPathDisplay\", \"System.Size\", " +
"\"System.DateCreated\", \"System.Author\", \"System.Keywords\"";
private const string DOCUMENT_COLUMNS = "\"System.Document.PageCount\", \"System.Document.ParagraphCount\", \"System.Document.WordCount\"";
private const string IMAGE_COLUMNS = "\"System.Image.HorizontalSize\", \"System.Image.VerticalSize\", \"System.Image.BitDepth\", " +
"\"System.Image.Compression\", \"System.Photo.CameraModel\", \"System.Photo.DateTaken\", \"System.Photo.Flash\"";
private const string MUSIC_COLUMNS = "\"System.Music.Artist\", \"System.Music.Genre\", \"System.Music.TrackNumber\", \"System.Audio.Compression\", " +
"\"System.Audio.SampleRate\", \"System.DRM.IsProtected\", \"System.Music.AlbumTitle\", \"System.Rating\", \"System.Audio.EncodingBitrate\""
;

private
const string VIDEO_COLUMNS = "\"System.RecordedTV.ChannelNumber\", \"System.RecordedTV.EpisodeName\", \"System.RecordedTV.NetworkAffiliation\", " +
"\"System.RecordedTV.RecordingTime\", \"System.Video.Compression\", \"System.Video.EncodingBitrate\", \"System.Video.FrameHeight\", \"System.Video.FrameWidth\""
;

Visual Basic

Dim GENERAL_COLUMNS As String = """System.DateModified"", ""System.DateCreated"", ""System.Kind"", ""System.Title"", ""System.Size"", ""System.Author"", ""System.Keywords""" 

Dim DOCUMENT_COLUMNS As String = """System.Document.PageCount"", ""System.Document.ParagraphCount"", ""System.Document.WordCount""" 

Dim IMAGE_COLUMNS As String = """System.Image.HorizontalSize"", ""System.Image.VerticalSize"", ""System.Image.BitDepth"", ""System.I" & _ 
"mage.Compression"", ""System.Photo.CameraModel"", ""System.Photo.DateTaken"", ""System.Photo.Flash""" 

Dim MUSIC_COLUMNS As String = """System.Music.Artist"", ""System.Music.Genre"", ""System.Music.TrackNumber"", ""System.Audio.Compres" & _ 
"sion"", ""System.Audio.SampleRate"", ""System.DRM.IsProtected"", ""System.Music.AlbumTitle"", ""Syst" & _ 
"em.Rating"", ""System.Audio.EncodingBitrate""" 

Dim VIDEO_COLUMNS As String = """System.RecordedTV.ChannelNumber"", ""System.RecordedTV.EpisodeName"", ""System.RecordedTV.NetworkAf" & _ 
"filiation"", ""System.RecordedTV.RecordingTime"", ""System.Video.Compression"", ""System.Video.Encod" &_ 
"ingBitrate"", ""System.Video.FrameHeight"", ""System.Video.FrameWidth""" 

The first, GENERAL_COLUMNS, contains column names that should be present for most files. When a file is selected, a query is always run to retrieve the values for the general columns. The document, image, music, and video column lists are then used as necessary. This isn't actually necessary. If you query for System.Video.FrameHeight from a Microsoft Word document, you'll just get a NULL return value. For clarity in a sample application, I decided to separate them and run the query twice: first for the general columns, then for the type-specific columns. The System.ItemNameDisplay property returns the final part of the filename (excluding the path).

Many columns are self-explanatory, but an important one to know is System.Kind (which you may have noticed in the prior query). This returns an identifier based on how WDS identifies the file. Image files like JPEG, GIF, TIFF, and more have a kind of “picture.”  Music files return “music” and video files "video".  For the most part, this just makes sense. Be aware of this as it can make a big difference in relevant results. You could specify a WHERE clause such as “System.Kind = ‘document'”, or use the CONTAINS keyword for typically better performance, “CONTAINS(“System.Kind”, ‘document')”.

The method to obtain the file properties is a little more complicated than the one used for search results. This is because all of the results are copied into a Dictionary object with a string key and string value. You may have noticed that almost all of the properties begin with “System.” I strip that out to clean up the list a bit. I also filter out NULL values, and convert arrays into comma-separated lists of string. It gets a little messy, but the results are worth it.

C#

public static Dictionary<string, string> ObtainProperties(string columns, string filename)
{
    Dictionary<string, string> items = new Dictionary<string, string>();

    filename = filename.Replace("'", "''");

    OleDbCommand cmd = new OleDbCommand();
    cmd.Connection = conn;
    cmd.CommandText = string.Format(
        "SELECT {0} from systemindex WHERE System.ItemNameDisplay = '{1}'",
        columns, filename);

    OleDbDataReader results = cmd.ExecuteReader();

    string key, value;

    if (results.Read())
    {
        for (int i = 0; i < results.FieldCount; i++)
        {
            if (!results.IsDBNull(i))
            {
                key = results.GetName(i);

                if (results.GetValue(i) is string[])
                {
                    string[] arrayValue = (string[])(results.GetValue(i));

                    if (arrayValue.GetLength(0) == 1) value = arrayValue[0];
                    else value = string.Join(", ", arrayValue);
                }
                else value = results.GetValue(i).ToString();

                if (key.StartsWith("System.")) key = key.Substring(7);
                items.Add(key, value);
            }
        }
    }

    results.Close();
    return items;
}

Visual Basic

Public Shared Function ObtainProperties(ByVal columns As String, ByVal filename As String) As Dictionary(Of String, String) 

  Dim items As Dictionary(Of String, String) = New Dictionary(Of String, String) 

  filename = filename.Replace("'", "''") 

  Dim cmd As OleDbCommand = New OleDbCommand 
  cmd.Connection = conn 
  cmd.CommandText = String.Format("SELECT {0} from systemindex WHERE System.ItemNameDisplay = '{1}'", columns, filename) 

  Dim results As OleDbDataReader = cmd.ExecuteReader 
  Dim value As String 
  Dim key As String 

  If results.Read Then 
    Dim i As Integer = 0 

    Do While (i < results.FieldCount) 
      If Not results.IsDBNull(i) Then 
        key = results.GetName(i) 

        If (TypeOf (results.GetValue(i)) Is String()) Then 

          Dim arrayValue() As String = CType(results.GetValue(i), String()) 

          If (arrayValue.GetLength(0) = 1) Then 
            value = arrayValue(0) 
          Else 
            value = String.Join(", ", arrayValue) 
          End If 
        Else 
          value = results.GetValue(i).ToString 
        End If 

        If key.StartsWith("System.") Then 
          key = key.Substring(7) 
        End If 

        items.Add(key, value) 
      End If 

      i = i + 1 
    Loop 
  End If 

  results.Close() 
  Return items 
End Function 

Creating the User Interface

In order to create a good user interface, I wanted to create something that would drop into place, and hopefully be reusable too. To that end, I created two custom controls. The main control AutoSearchBox combines a TextBox control and a ListBox control. It hooks into the Changed event of the TextBox in order to issue search commands to the background thread. The PreviewKeyDown event catches key presses as they happen and sees the raw value. This allows it to catch the UP and DOWN arrows and the RETURN key. As the user enters a search term, all files containing that term in the filename continually update in the list below.

Clicking Go causes the details to be retrieved and displayed. I wanted to just be able to set the dictionary collection as a data source in some control to view the values, but that didn't seem possible. The PropertyViewer control works well if you want to pass it a business object and display its properties, but for dictionary objects containing key-value pairs, I didn't see an easy solution. This led me to create the DictionaryViewer user control. Pass it a collection and a grouping name, and it displays the keys and values in columns under the given group name. I first pass it the general properties, then the file type-specific properties. This works well and looks pretty good.  Feel free to integrate it in your applications, extend it, or even rewrite it!

The Search Thread

The WDSAutoSearch object is responsible for the background searches. It has no connection to any user interface controls, but rather works through methods and events. When you are ready to perform a search, simply call the Update method with a string. This doesn't return any search results though. In order to retrieve the search results, respond to the ItemsChanged event, then query the Items property for the search values.

When the WDSAutoSearch object is created, it starts a new Thread set to background. This is important so it doesn't need to be explicitly stopped when closing the application. The background thread runs the UpdateStringsWorker method. This runs a while loop processing searches. This could be a problem for performance if it just kept looping, even when searches weren't requested, but most of the time it sleeps.

The AutoResetEvent object is a thread coordinator. One thread can call Wait and be put to sleep. When another thread calls Set on the same AutoResetEvent object, the waiting thread wakes up. So, when the while loop begins, it calls Wait. When another object invokes the Update method to perform a search, it actually just calls Set to wake up the background worker. When the background worker is done with a search, it raises the ItemsChanged event so any interested parties can update based on the results. Very elegant, and efficient.

Next Steps

As mentioned, this could easily be converted into a launcher application. Use the Process.Start method. In order to obtain the full path to a file, access the System.ItemPathDisplay column. Even passing an image or document works with Process.Start. It will automatically launch the system default application for the file type. The only caveat is for shortcuts (.lnk files). If System.Kind is “link” then the System.ItemPathDisplay will only show you where the .lnk file was. To get to the path pointed to by the shortcut, reference System.Link.TargetParsingPath.

Conclusion

Desktop searching is powerful stuff. By including WDS 3.0 with Windows Vista, you can write to it, knowing that it is always available. Have fun coming up with ways to leverage this technology to simplify locating files in your applications. Download Visual Studio Express and give it a try!

Tags:

Follow the Discussion

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.