Adding a New Search Condition to FileSharper - adv12/FileSharper GitHub Wiki

Adding a new search condition to FileSharper is relatively simple. It typically only requires you to add two classes: one implementing ICondition and one with public properties for any parameters used by the condition. It's not necessary to add anything manually to the user interface; FileSharper uses reflection to build a user interface at runtime for any conditions it finds in the FileSharperCore assembly.

This tutorial will walk through the process of adding a "File Date" condition from scratch. (This condition already exists in the repository but will be recreated for purposes of explanation.) The condition will compare a file's created, modified, or accessed date to a date provided by the user to test whether the date is before or after the specified date.

Load FileSharper.sln in Visual Studio 2017 and Look Around

If you don't already have the FileSharper source, download or clone it from https://github.com/adv12/FileSharper. If you downloaded a zip, unzip it. Open the src/FileSharper directory and double-click FileSharper.sln to open it in Visual Studio. Solution Explorer should look something like this:

Initial Solution Explorer

FileSharper should be set as the startup project. The business logic is all in FileSharperCore. Expand FileSharperCore:

FileSharperCore Expanded

The main interfaces are in the root of FileSharperCore. Concrete implementations are organized under subdirectories. There are subdirectories for the four main types of "pluggable items": FileSources, Conditions, FieldSources, and Processors.

Add a New Condition Class

We will be implementing the ICondition interface by extending the ConditionBase abstract class. Concrete implementations of ICondition are organized under the Conditions directory. Because our condition tests a file system property, we will put it in the Filesystem subdirectory:

Conditions/Filesystem

Add a class by right-clicking Filesystem and selecting "Add->Class...". Name the class "FileDateCondition.cs". You should see something like this in Visual Studio:

New Condition Class

Extend ConditionBase

Make the class public if it isn't already, and make it extend ConditionBase:

Extending ConditionBase

(You can implement ICondition directly, but usually ConditionBase gives you a headstart.)

Visual Studio will now complain about unimplemented abstract members. Right-click the red squiggly and select "Implement Abstract Class." Your class will now look something like this:

ConditionBase Implemented

Set the Category, Name, and Description

Every condition should report its category, name, and description. The Category property is used to group similar conditions in the user interface. The Name property is the display name for the property in the user interface. The Description property is currently unused but will potentially be shown in the UI to give a longer description than that provided by Name.

For this condition, we'll set Category to "Filesystem," Name to "File Date", and Description to "File date compares to the specified date":

Category, Name, and Description

Create a Class for the Parameters

We want the user to be able to select which type of file date to look at (created, modified, or accessed), what date to compare to, and what comparison operator to use. Every ICondition has a public property of type object called Parameters. The user interface uses reflection to present the public properties of the Parameters object in a property editor. So getting user input for a condition is a simple matter of defining a class that contains the desired properties and returning it from the Parameters getter.

Rather than create a whole new file for the parameters, FileSharper code has a convention of defining a condition's related parameters class in the same file as the class that uses it. So add the following class as a sibling to the condition class in the file we've been editing:

public class FileDateComparisonParameters
{
    public FileDateType FileDateType { get; set; }
    public TimeComparisonType ComparisonType { get; set; }
    public DateTime Date { get; set; } = DateTime.Now;
    public string OutputFormat { get; set; } = "yyyy/MM/dd hh:mm:ss tt";
}

(Notice that two of the properties (FileDateType and ComparisonType) are of enum type. The enums used here are defined in a file called Enums.cs at the root of FileSharperCore. You may need to define new enums for parameter types you add yourself.)

Next, add a member variable named m_Parameters of type FileDateComparisonParameters to the FileDateCondition class and update the Parameters property to return it:

private FileDateComparisonParameters m_Parameters = new FileDateComparisonParameters();
...
public override object Parameters => m_Parameters;

The file should now look something like this:

Parameters

The user interface control used to display the parameters for editing will, by default, show them in alphabetical order. To show properties in another order, it must be provided with explicit ordering information via attributes. This results in an unfortunate sprinkling of UI-related code in the FileSharperCore library. To specify the display order of the properties in FileDateComparisonParameters, add this using statement to the top of the file:

using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;

Then decorate the properties with attributes like this:

public class FileDateComparisonParameters
{
    [PropertyOrder(1, UsageContextEnum.Both)]
    public FileDateType FileDateType { get; set; }
    [PropertyOrder(2, UsageContextEnum.Both)]
    public TimeComparisonType ComparisonType { get; set; }
    [PropertyOrder(3, UsageContextEnum.Both)]
    public DateTime Date { get; set; } = DateTime.Now;
    [PropertyOrder(4, UsageContextEnum.Both)]
    public string OutputFormat { get; set; } = "yyyy/MM/dd hh:mm:ss tt";
}

FileDateComparisonParameters needs one more minor tweak to work correctly in the user interface: an Editor attribute that serves as a hint to the UI as to what type of editor to show for the Date property. Add this line above the declaration of the Date property:

    [Editor(typeof(DateTimePickerEditor), typeof(DateTimePicker))]

Handle Columns

Conditions can return zero or more informational columns in addition to reporting whether they match a particular file. There are two column-related properties we have yet to implement: ColumnCount and ColumnHeaders.

In addition to reporting whether a file's date matched the user's specifications, it's useful to report the file's date itself. So we will report one column of data in our results. FileSharper needs to know at initialization of a search how many columns a condition will generate. So we return "1" from the ColumnCount property:

public override int ColumnCount => 1;

We also need to report the column names. Because we're allowing the user to choose which type of date to test (created, modified, or accessed), we can return a column header that reflects the user's choice. So we implement ColumnHeaders like this:

    public override string[] ColumnHeaders
    {
        get
        {
            string headerText = string.Empty;
            switch (m_Parameters.FileDateType)
            {
                case FileDateType.Created:
                    headerText = "Created Date";
                    break;
                case FileDateType.Modified:
                    headerText = "Last Modified Date";
                    break;
                default:
                    headerText = "Last Accessed Date";
                    break;
            }
            return new string[] { headerText };
        }
    }

Implement the Matches Method

The only thing left to do is to implement the Matches method that tests whether a file matches the condition. Here is the full implementation for this condition:

    public override MatchResult Matches(FileInfo file, Dictionary<Type, IFileCache> fileCaches, CancellationToken token)
    {
        DateTime fileDate;
        switch (m_Parameters.FileDateType)
        {
            case FileDateType.Created:
                fileDate = file.CreationTime;
                break;
            case FileDateType.Modified:
                fileDate = file.LastWriteTime;
                break;
            default:
                fileDate = file.LastAccessTime;
                break;
        }
        bool matches = false;
        switch (m_Parameters.ComparisonType)
        {
            case TimeComparisonType.After:
                matches = (fileDate > m_Parameters.Date);
                break;
            default:
                matches = (fileDate < m_Parameters.Date);
                break;
        }
        MatchResultType resultType = matches ? MatchResultType.Yes : MatchResultType.No;
        return new MatchResult(resultType, new string[] { fileDate.ToString(m_Parameters.OutputFormat) });
    }

The logic isn't complicated, but the method signature and return value are worth discussion.

Matches is called once for each file tested. The FileInfo passed into Matches is the file to test. The fileCaches parameter is a way to cache data for sharing between conditions for performance reasons and can be ignored for the purpose of this tutorial. The CancellationToken is for allowing the user to exit your condition early if it is time-consuming to run. It can also be ignored for now.

Matches returns a MatchResult object, which has a MatchResultType (Yes, No, or NotApplicable) and an array of values corresponding to the columns discussed above.

You should now be able to build and run FileSharper and see your condition in action:

New Condition Appears

editingNewCondition