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:
FileSharper should be set as the startup project. The business logic is all in FileSharperCore. Expand FileSharperCore:
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:
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:
Extend ConditionBase
Make the class public if it isn't already, and make it extend 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:
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":
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:
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 };
}
}
Matches
Method
Implement the 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: