Pages: << 1 ... 92 93 94 95 96 97 98 99 100 101 102 ... 105 >>

03/03/05

Permalink 06:08:58 pm
Categories: Programming, .NET, Software

An implementation of a Copernic Desktop Search Custom Extractor in C#

As I mentioned in a previous post, Copernic Desktop Search 1.5 beta is currently available to the public. One of the most important new features was for me the introduction of an extensibility API.

When I wrote that first announcement, I hadn't had a close look at the API. By now I've found out that it's about extracting data from new file types, nothing more or less than that. I'd wish, and maybe I should add that to my list of things I'd like to see in CDS, they would extend that extensibility support to other areas, like creating plugins for file type preview, or even introducing completely new ranges of searchable objects. Well, maybe that's in the future.

For now, I've taken the plunge and tried to implement a custom extractor for CDS. And while I was at it, I wanted to do it in in C#. I succeeded and these are the results, maybe somebody will find this useful to implement a custom extractor that really does something worthwhile :-)

The interface

The first task was to create a C# interface from the IDL description that's given in the API documentation. My experience with COM and ActiveX stems from work I've been doing in C++ and Delphi some years back, I've been doing close to nothing with .NET InterOp, so I'm no expert at this. Nevertheless, I managed to create the following definition:

[ComVisible(true), ComImport, Guid("7E337435-5E47-40A0-B8A9-315BDD1BAE0D"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICopernicDesktopSearchFileExtractor {
  [DispId(1)] 
  void LoadURI([MarshalAs(UnmanagedType.BStr)] string uri);
  [DispId(5)] 
  void GetContentStream([MarshalAs(UnmanagedType.IUnknown)] out object contentStream);
  [DispId(9)] 
  void get_IsContentUnicode([MarshalAs(UnmanagedType.VariantBool)] out bool value);
}

The implementation

The next job was to create an implementation of the interface in a COM server. BTW, the assembly you use for this must have the Register for COM Interop flag set. This is my implementation:

[ClassInterface(ClassInterfaceType.None), Guid("1bb4b2a5-d516-4a00-868b-cfc49a84881a")]
public class CustomExtractor: ICopernicDesktopSearchFileExtractor {
  void ICopernicDesktopSearchFileExtractor.LoadURI(string uri) {
    currentURI = uri;
  }

  private string currentURI;

  void ICopernicDesktopSearchFileExtractor.GetContentStream(out object contentStream) {
    contentStream = null;
    if (currentURI != null && File.Exists(currentURI)) 
      contentStream = new IStreamWrapper(File.OpenRead(currentURI));
  }

  void ICopernicDesktopSearchFileExtractor.get_IsContentUnicode(out bool value) {
    value = false; // return true if the file contains Unicode content
  }

  const string regPath = @"SOFTWARE\Copernic\DesktopSearch\CustomExtractors";

  [ComRegisterFunction]
  public static void Register(Type t) { 
    RegistryKey rkey = Registry.LocalMachine.CreateSubKey(regPath);
    rkey.SetValue(".testfile", "{1bb4b2a5-d516-4a00-868b-cfc49a84881a}", 
      RegistryValueKind.String);
    rkey.Flush( );
  }

  [ComUnregisterFunction]
  public static void Unregister(Type t) {
    RegistryKey rkey = Registry.LocalMachine.CreateSubKey(regPath);
    rkey.DeleteValue(".testfile", false);
    rkey.Flush( );
  }
}

This implementation registers itself for a ".testfile" filetype. In reality, I simply created a few text files and renamed them to that extension.

The stream wrapper

As you can see, there's a class in use called IStreamWrapper. This is another class I had to write because CDS wants to access the extracted data from the file using the COM interface IStream. The .NET System.IO.Stream doesn't implement that interface, so I needed a wrapper class for a .NET stream that I could expose via COM. Luckily, I found that the implementation was rather straightforward and that CDS only calls two methods from the IStream interface during a normal run, so I didn't need to implement many of the methods. Here's what I came up with:

public class IStreamWrapper : IStream {
  public IStreamWrapper(Stream stream) {
    if (stream == null)
      throw new ArgumentNullException("stream", "Can't wrap null stream.");
    this.stream = stream;
  }

  Stream stream;

  public void Clone(out System.Runtime.InteropServices.ComTypes.IStream ppstm) { 
    ppstm = null;
  }

  public void Commit(int grfCommitFlags) { }

  public void CopyTo(System.Runtime.InteropServices.ComTypes.IStream pstm, 
    long cb, System.IntPtr pcbRead, System.IntPtr pcbWritten) { }

  public void LockRegion(long libOffset, long cb, int dwLockType) { }

  public void Read(byte[] pv, int cb, System.IntPtr pcbRead) {
    Marshal.WriteInt64(pcbRead, (Int64) stream.Read(pv, 0, cb));
  }

  public void Revert( ) { }

  public void Seek(long dlibMove, int dwOrigin, System.IntPtr plibNewPosition) {
    Marshal.WriteInt64(plibNewPosition, stream.Seek(dlibMove, (SeekOrigin) dwOrigin));
  }

  public void SetSize(long libNewSize) { }

  public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag) {
    pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG( );
  }

  public void UnlockRegion(long libOffset, long cb, int dwLockType) { }

  public void Write(byte[] pv, int cb, System.IntPtr pcbWritten) { }
}

The only methods that CDS really uses are Seek and Read.

The test setup

To try things out, I shut down CDS and moved my old index files away. Then I changed the settings for folders indexed to include only the one folder where I had created the test files with the .testfile extension. I compiled and registered the library with the classes above (for registration of a .NET assembly you use regasm, not regsvr32 as the documentation describes) and startup CDS again. I found the most reliable way to get CDS to reconstruct its index was to use the link on the Advanced page of the options dialog, that says "Clear index contents and reindex all files and folders". The suggestion from the documentation didn't work reliably for me, to use the Options/Update index/Files menu entry.

Once everything was in place, I found the whole mechanism working flawlessly. CDS would index my files and I could find them when entering search words that I knew were in the files. The only fly in the ointment, as I already hinted above, is that CDS doesn't provide any kind of preview for custom file types, and there's no (public?) way to extend it in that regard. Apart from that, thanks to Copernic for listening to those of your users who suggested an extensibility feature!

Permalink 01:00:23 pm
Categories: Programming, .NET

Order of properties in ITypedList implementations

In a comment about my post Simulating object properties with ITypedList and custom PropertyDescriptors, John Eyles mentioned the fact that the TypeDescriptor.GetProperties method doesn't return the type's properties in any fixed order. Therefore, when my own ITypedList implementation takes at least parts of its properties from a standard set that was fetched via the TypeDescriptor, additional steps have to be taken if the properties are supposed to appear in a particular order, like in a grid or similar. Wouldn't it be nice if the properties could be ordered from the beginning? Actually, I had already implemented this in the past, so I thought I'd just put up another post to show how it can be done.

Parts of the solution

The final solution is comprised of several parts, which I'll explain one after the other. In short, there are an attribute to decorate properties with the desired sort order (and, as an add-on, whether to show the property at all), a wrapper class for a property descriptor that makes it sortable based on a sort index and, last and most important, a helper class that uses the attributes and the sortable wrapper to return pre-sorted property collections. The helper class also implements a simple cache, so this doesn't become too inefficient if used often.

The attribute

The attribute allows the user to decorate properties of data classes, giving information about the desired sort order and whether or not to make a property visible. Here's the code for that:

[AttributeUsage(AttributeTargets.Property, 
  AllowMultiple=false, Inherited=true)]
public class BindingInfoAttribute : Attribute {
  public BindingInfoAttribute() {
    this.visible = true;
  }
  
  public BindingInfoAttribute(bool visible) {
    this.visible = visible;
  }

  private bool visible;
  public bool Visible {
    get { return visible; }
    set { visible = value; }
  }

  private int sortIndex;
  public int SortIndex {
    get { return sortIndex; }
    set { sortIndex = value; }
  }
}

The property descriptor wrapper

To be able to sort a list of property descriptors according to the SortIndex given in the BindingInfoAttribute, we need this wrapper class. It stores a sort index and implements the IComparable interface to make use of it during sorting. Note that this class doesn't derive from PropertyDescriptor, so maybe the name isn't very fitting, but I wasn't going to change that now.

public class SortablePropertyDescriptor : IComparable {
  public SortablePropertyDescriptor(PropertyDescriptor descriptor, int sortIndex) {
    this.descriptor = descriptor;
    this.sortIndex = sortIndex;
  }

  private PropertyDescriptor descriptor;
  public PropertyDescriptor Descriptor {
    get { return descriptor; }
  }

  private int sortIndex;
  public int SortIndex {
    get { return sortIndex; }
  }

  public int CompareTo(object obj) {
    SortablePropertyDescriptor other = obj as SortablePropertyDescriptor;
    if (other == null) throw new ArgumentException(
      "The given object is not of type SortablePropertyDescriptor.", "obj");
    return this.SortIndex - other.SortIndex;
  }
}

The helper class

The PropertyDescriptorCollectionHelper has the most important job, proving the GetPropertyCollection method. This method looks at all the properties of the given type, searching for binding information given by the BindingInfoAttribute. If such information is found, it's used to define which properties to include in the result collection and also the sort order of the properties. Using the parameters doSort and skipCache, the caller can decide whether sorting should be skipped and whether to use the internal descriptor collection cache.

The implementation uses generic collection types, but for .NET 1.1 the same thing should work just fine using ArrayLists in the places of the typed generic collections.

public class PropertyDescriptorCollectionHelper {
  public PropertyDescriptorCollectionHelper() {
    cache = new Dictionary<Type,PropertyDescriptorCollection>();
  }

  private static PropertyDescriptorCollectionHelper current;
  public static PropertyDescriptorCollectionHelper Current {
    get {
      if (current == null)
        current = new PropertyDescriptorCollectionHelper();
      return current;
    }
    set { current = value; }
  }

  private Dictionary<Type, PropertyDescriptorCollection> cache;

  public PropertyDescriptorCollection GetPropertyCollection(Type type, 
    bool doSort) {
    return GetPropertyCollection(type, doSort, false);
  }
		
  public PropertyDescriptorCollection GetPropertyCollection(Type type) {
    return GetPropertyCollection(type, true, false);
  }
		
  public PropertyDescriptorCollection GetPropertyCollection(Type type, 
    bool doSort, bool skipCache) {
    PropertyDescriptorCollection result = skipCache ? null : 
      cache.ContainsKey(type) ? cache[type] : null;
			
    if (result == null) {
      PropertyDescriptorCollection origProperties = 
        TypeDescriptor.GetProperties(type);
      List<PropertyDescriptor> resultList = doSort ? 
        null : new List<PropertyDescriptor>( );
      List<SortablePropertyDescriptor> sortList = doSort ? 
        new List<SortablePropertyDescriptor>() : null;

      foreach (PropertyDescriptor descriptor in origProperties) {
        BindingInfoAttribute bindingInfo = descriptor.Attributes[
          typeof(BindingInfoAttribute)] as BindingInfoAttribute;
        if (bindingInfo == null || bindingInfo.Visible) {
          if (doSort)
            sortList.Add(new SortablePropertyDescriptor(descriptor, 
              bindingInfo != null ? bindingInfo.SortIndex : int.MaxValue));
          else
            resultList.Add(descriptor);
        }
      }

      if (doSort) {
        sortList.Sort();
        resultList = ReduceSortableList(sortList);
      }

      result = new PropertyDescriptorCollection(resultList.ToArray());

      cache[type] = result;
    }

    return result;
  }

  private List<PropertyDescriptor> ReduceSortableList(
    List<SortablePropertyDescriptor> sortList) {
    List<PropertyDescriptor> resultList = new List<PropertyDescriptor>();
    foreach (SortablePropertyDescriptor desc in sortList)
      resultList.Add(desc.Descriptor);
    return resultList;
  }
}

How do I use that?

Now, to actually use these classes with your own data objects, you need to decorate your properties with the BindingInfoAttribute, like this:

public class DataClass {
  private string stringProperty1;
  [BindingInfo(SortIndex=2)]
  public string StringProperty1 {
    get { return stringProperty1; }
    set { stringProperty1 = value; }
  }

  private int intProperty1;
  [BindingInfo(SortIndex=1)]
  public int IntProperty1 {
    get { return intProperty1; }
    set { intProperty1 = value; }
  }

  private double doubleProperty1;
  [BindingInfo(Visible=false)]
  public double DoubleProperty1 {
    get { return doubleProperty1; }
    set { doubleProperty1 = value; }
  }
}

To retrieve a sorted collection of property descriptors for DataClass, you only need one line:

PropertyDescriptorCollection properties = 
  PropertyDescriptorCollectionHelper.Current.GetPropertyCollection(typeof(DataClass));

Have fun!

Permalink 11:16:20 am
Categories: General, Software

What I'd like to see in Copernic Desktop Search

I've been using Copernic Desktop Search for several months now and I think it's really a great program, especially as it is free. I've recently tried X1, to see if I'm missing anything, but I decided that for the steep price there's not enough X1 can offer me. This is even more true for the new (beta) version 1.5 of CDS, where a lot of features have been added.

Although things have been extended a lot, there are still a lot of features that are restricted for no apparent reason. Here's my list of things I'd like to see implemented/changed in CDS. In case anybody at Copernic reads this... personally, I'd be willing to pay a price for CDS, it doesn't have to be free. I also have licenses for Agent Professional, Tracker and Summarizer. Just don't make it as expensive as X1 :-)

  • Search overall. Why do I always have to specify first which kind of file I'm searching for?
  • For Thunderbird, index at least all locally stored folders, including offline folders. I'm using Thunderbird with IMAP, and apart from several weirdly named folders (like "02c67ac5"), I can only select my RSS folders for indexing, nothing else. Some of my IMAP and news group folders are offline folders and could be indexed as such.
  • For Outlook, index appointments and notes. I don't use Outlook for mail or contacts, so that support is useless for me.
  • Index favourites and history for more than one browser. Who uses just one browser these days? Why does there have to be a restriction to only one browser?
  • Indexing of OneNote data. At least this could be added by a custom extractor (I have an upcoming post on that) in the future.
  • Indexing of IMAP mail servers.
  • Configuration for the allowance of system load. Currently I can't influence when CDS thinks my system's resources are highly used, and therefore stops indexing. There are often very many processes running on my system and CDS stops indexing much too often. Nevertheless, generally the Copernic implementation of unintrusive background indexing works great, compared with X1 again.

02/03/05

Permalink 04:32:33 pm
Categories: Programming, .NET

Compiling Developer Express libraries for VS.NET 2005

I've been using Developer Express components with VS.NET 2005 (beta 1) for a while, but nearly always in forms that I had previously created with VS.NET 2003. Now, suddenly some problems showed up when these old forms were edited, specifically with XtraGrids and XtraEditors on them.

These problems were quite severe, as the VS designer would simply refrain from persisting all properties that are stored in "inner objects" of the main component. This concept is used all over the DX libraries, for example for the Properties property in the XtraEditors library and the Styles and Views properties of the GridControl. A single modification in the designer would trigger recreation of the InitializeComponent method, thereby effectively destroying the form.

The funny (and good) thing about this is that I found the necessary modifications in the source code files already. As far as I know, DX distribute compiled versions for VS.NET 2005 to the customers that need them, and I guess these versions are actually compiled with the modifications enabled. Using the standard distribution, on the other hand, these modifications are not active by default!

What you need to do is this: Add a constant DXWhidbey to the constant list of the configuration you are compiling for, for all the projects in the DX source code. If you do this within VS, it should look like this (for the Debug configuration):

You can also edit the .csproj file yourself (or use search/replace), in that case the changed line should look like this:

<DefineConstants>DEBUG;TRACE;DXWhidbey</DefineConstants>

Recompile all the libraries and voilá: Working persistence in VS.NET 2005.

01/03/05

Permalink 02:14:28 pm
Categories: Programming, .NET, Pocket PC

CERapi update

I linked to Gaurav Khanna's first article about his managed Windows CE Remote API in a previous post. Now there's an update available. Details in his new blog post.

<< 1 ... 92 93 94 95 96 97 98 99 100 101 102 ... 105 >>

Enter your email address:

Search

Oliver
MVP logo
May 2015
Sun Mon Tue Wed Thu Fri Sat
 << <   > >>
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31