My father, who calls himself a “fact collector”, has been collecting facts relating to a wide selection of topics for over 20 years now. He has been storing all of his text in several text files, and each quote or article he stores, he stores in a plain text file, delimited by two asterisks on a line by itself, as follows:
Hello, I am fact number 1.
**
Hello, this is the second fact.
This second fact is second only to the first fact,
which is fact number 1.
**
This is fact number three.
These files can grow to be up to 10 megs in size, and he has over 200 megs worth of these files. He wrote a quick and dirty VB app last year to access these files, but the search process was extremely slow, so I rewrote the application in C# and offered it to him as an alternative, which he is now using.
The search system needed to be set up in a few ways.
There are a few objects in this code I will discuss.
The NoteFileInfo is the object that stores the categories, file locations, and the current category selections for the application. It is serializable, and can be stored on the hard drive. When the application is first started, it looks for default.nfb, and when it closes, it prompts to save to default.nfb. The object can be edited, categories and files added, by using the Edit FileBase menu selection on the menu. This window is pretty self-explanatory when used. This object uses Singleton pattern. The following is the serialization code for the NoteFileInfo object:
NoteFileInfo
#region serialization
/// <summary>
/// Loads a NoteFileInfo class from a binary
/// formatted file on the hard drive.
/// </summary>
/// <param name="filename">The name of the
/// file to be loaded. </param>
public static void Load(string filename) {
if (new FileInfo(filename).Exists)
using (Stream st = (Stream)new FileStream(
filename, FileMode.Open))
_noteFileInfo =
(NoteFileInfo)new BinaryFormatter().Deserialize(st);
}
/// <summary>
/// Saves the Notefile class to a binary
/// formatted file on the hard drive.
/// </summary>
/// <param name="filename">The name of the
/// file to be saved. </param>
public static void Save(string filename) {
_noteFileInfo._dirty = false;
if (new FileInfo(filename).Exists)
new FileInfo(filename).Delete();
using (Stream st = (Stream)new FileStream(filename,
FileMode.OpenOrCreate))
new BinaryFormatter().Serialize(st, _noteFileInfo);
}
/// <summary>
/// Simply "closes" the notefile by setting the
/// internal _noteFileInfo to a new object. (See singleton class).
/// </summary>
public static void Close() {
_noteFileInfo = new NoteFileInfo();
}
#endregion
The NoteFile object is actually a collection of the strings within a particular file. For example, from the example of the file above, there would be three strings in the NoteFile object. The code is simple, and does nothing more than stream read through the file and add the items to the list.
NoteFile
The SearchResults object multithreads itself when running, so it can run in the background, and thus not tie up the UI during its running. It will send back events to the UI, however, to update the UI on its current status. Notice the “NoteFileInfo.getInstance()” call to the singleton NoteFileInfo class. The design of this class enables the UI to call the class using the Run() and Stop() methods, but still allows the class to place itself in another thread. This way, the UI can call the Stop method at any time, and the class will take care of its own abort.
SearchResults
NoteFileInfo.getInstance()
Run()
Stop()
Stop
namespace Notes2007 {
class SearchResults : List<string> {
SearchTerms searchTerms;
Thread thread;
public delegate void ResultAdded(string result);
public event ResultAdded NewResult;
public delegate void NowSearching(string filename);
public event NowSearching Searching;
public delegate void Done();
public event Done Finished;
public SearchResults(SearchTerms searchTerms) {
this.searchTerms = searchTerms;
}
public void Run() {
ThreadStart ts = new ThreadStart(DoSearch);
thread = new Thread(ts);
thread.IsBackground = true;
thread.Start();
}
private void DoSearch() {
foreach (string filename in
NoteFileInfo.getInstance().ActiveFiles) {
GC.Collect();
GC.WaitForPendingFinalizers();
Searching(filename.Substring(filename.LastIndexOf(@"\") + 1));
NoteFile notefile = new NoteFile(filename);
foreach (string note in notefile) {
bool add = true;
foreach (string searchterm in searchTerms) {
if (!(note.ToUpper().Contains(searchterm.ToUpper())))
add = false;
}
if (add) {
this.Add(note);
NewResult(filename.Substring(
filename.LastIndexOf(@"\") + 1) + "|" + note);
}
}
}
Finished();
}
public void Stop() {
thread.Abort();
Finished();
}
}
}
This is nothing more than a word parser, which parses the words searched and adds them to a list of strings. The object listed below also excludes all words shorter than three characters long, as well as the two common words “the” and “and”. The object will allow phrases as a single search term as well, by simply enclosing the phrase in quotes.
namespace Notes2007 {
class SearchTerms : List<string> {
public SearchTerms(string searchstring) {
List<string> ignoredterms = new List<string>();
ignoredterms.Add("the");
ignoredterms.Add("and");
bool inquote = false;
string searchterm = string.Empty;
foreach (char c in searchstring.ToCharArray()) {
if (c.ToString() == @"""") {
inquote = !inquote;
continue;
}
if ((c.ToString() == " ") && (!inquote)) {
if (!this.Contains(searchterm) &&
(searchterm.Length > 2) &&
(!ignoredterms.Contains(searchterm)))
this.Add(searchterm);
searchterm = string.Empty;
continue;
}
searchterm += c;
}
if (!this.Contains(searchterm)) {
this.Add(searchterm);
searchterm = string.Empty;
}
}
}
}
The general idea of the application is that the user simply enters a search string, and then the app will find all the articles or quotes that contain all the search terms within the search string.
SearchTerms
Snippet
I’ve started using this myself as a great way to store code I’ve accumulated from CodeProject and other locations for easy searching later.