Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / Windows Forms

FileSelect - Hassle Free Implementation of the File Menu

Rate me:
Please Sign up or sign in to vote.
4.67/5 (15 votes)
4 May 2009CPOL13 min read 51K   512   38   7
A WinForms user control that implements the details of file handling commands for any document-centric application

FileSelect - A quick Implementation of the File Menu

fileselect.png

What It's For

FileSelect is a WinForms user control for a default implementation of the "File" menu. All you need to do is to implement opening, saving and closing a document and sending change notifications, and you will get:

  • Correct behavior of Save, Save As and Close - whether this document is new or opened, and whether it was modified.
    This includes asking if the file should be saved, asking for a file name, etc.
  • Correct handling of file modified / not modified
  • Disabling menu items when they can't be used
  • A Recent Files list - in place or as a popup menu
  • An automatically updating form title, including the current file name and an "is modified" marker
  • Customizable OpenFile / SaveFile dialogs included

Most of the functionality can be activated selectively. I've tried to keep you in control with the individual features.

The download contains a sample (FileSelectDemo) that implements a basic text editor - or rather, the file handling part - where you can explore the available functionality. It shows all commands available, including two styles of recent files. Of course, in your application you can add only the items you need.

How to Add It to your Project

Adding FileSelect to your project: Open the Toolbox panel, right click and select "Customize...". In the "Customize Toolbox" dialog, on the ".NET Framework components" tab, select "Browse", and select FileSelect.dll.  Deselect the "RecentFileList" and "Strings" controls (only add the FileSelect component itself), and click OK.

Adding FileSelect to your main form: From the toolbox add a FileSelect component, and a MenuStrip to your main application form (or wherever you need them). Add the desired commands to the menu.

Tip: Right click the MenuStrip component, and select "Add default items".

Select the "New" menu items, and change the "FileSelectCommand on fileSelect1" property in category "General" to "New". Do the same for all other commands you want (usually New, Open, Save, Save As and Close).
Similarly, you can wire up toolbar buttons.

Adding a Recent Files list: Insert a new placeholder menu item (it will never be visible), and assign the Recent FileSelect command to it. At runtime, it will be replaced by a list of recent files.

Additionally, if the placeholder is followed by a separator, and the list of recent files is empty, that separator is hidden. This allows the common style of putting the recent files list between two separators, without having two consecutive separators when there are no files. Similarly, if the placeholder is the only item in a popup menu, and the recent files list is empty, the parent item opening the popup menu gets disabled.

Implement the Commands: Select the FileSelect control, and go to the "Events" tab of the property panel. Add handler for the events NewDocument, OpenDocument, SaveDocument, CloseDocument. New and Open can usually be implemented in the same handler. The following examples use a simple TextDocument to be displayed in a TextBox (textbox1). TextDocument is included with FileSelect, for other file formats you need to  use your own document class and its serialization here.

Handle New and open document, and:

C#
private void fileSelect1_NewOrOpenDocument(object sender, EventArgs e)  
{
  // Handles "NewDocument" and "OpenDocument"

  FileSelectEventArgs fse = (FileSelectEventArgs)e;
  DocumentInfo docInfo = fse.DocumentInfo; 	// additional FileSelect data 
					// associated with your document

  // Create document instance
  TextDocument textDoc;
  if (fse.Command == EFSCommand.New)
    textDoc = TextDocument.New();           	// create an empty document
  else
    textDoc = TextDocument.Load(fse.Path);  	// ... or load from the specified path

  // if that is successful, associate the document info with your document object
  docInfo.InitDocument(textDoc);	    

  // update the user interface:
  textBox1.Text = textDoc.Data;   // set the text
  textBox1.Tag = docInfo;         // remember the document info (for the dirty flag)
  textBox1.ReadOnly = false;      // set the text box to read only, so it can be edited
}
C#
private void textBox1_TextChanged(object sender, EventArgs e)
{
  DocumentInfo docInfo = textBox1.Tag as DocumentInfo;
  if (docInfo != null)
    docInfo.IsDirty = true;
}

The Dirty Flag forwards any changes in the text box to the document info, so the user interface can be updated accordingly. Onward to the save handler:

C#
private void fileSelect1_SaveDocument(object sender, EventArgs e)
{
  // we update the document, and save to the path specified in the event args
  FileSelectEventArgs fse = (FileSelectEventArgs)e;
  DocumentInfo docInfo = fse.DocumentInfo;
  TextDocument doc = (TextDocument)docInfo.Document;

  // update the document with changes from the view, and save it
  doc.Data = textBox1.Text;
  doc.Save(fse.Path); // note: fse.Path may be different from docInfo.Path
  fse.SaveComplete = true; // indicate that save was successful
}

This event is used for Save, Save As and Save Copy As. The file name to save to is passed in fse.Path, and may be different from the documents file path.

Note: You need to set SaveComplete to true when saving the document was successful. If you don't, FileSelect assumes the save failed (e.g. because your spanking new 1TB disk is already full again...).

Finally, when the document is closed, the user interface must be updated. Also, when the main form is closed, we need to handle modified documents:

C#
private void fileSelect1_CloseDocument(object sender, EventArgs e)
{
  textBox1.Text = String.Empty;  	// clear text box
  textBox1.ReadOnly = true; 	// set to read only
  textBox1.Tag = null; 
}
C#
private void Form1_FormClosing(object sender, FormClosingEventArgs e) // handler for 
							// FormClosing event
{
  if (!fileSelect1.HandleQuit(e.CloseReason)) // ask user to close modified files, etc.
    e.Cancel = true;			// cancel closing the form if 
					// the user said "cancel".
}

Additional Features and Settings

Control Properties

The following settings can be changed in the FileSelect control properties. The values in parentheses are the default values.

UpdateContainerTitle (true) If this flag is true, the title of ContainerControl is adjusted to display the current document and the modified state. Alternatively or additionally, you can handle the ParentTitleChanged event if you need custom handling.
AllowSaveUnmodified (false) Enable the "Save" command even if the document is not modified. This is uncommon, but may be desired for some applications.
AskCloseUnmodified (false) When closing a document that is not modified, the user is asked to confirm and can cancel closing. This is uncommon, but may be desired for some applications. This option does not affect the message that is shown when a modified document should be closed.
CloseCreatesNew(false) Instead of having no document open, a new document is created. This is the same behavior as in Notepad, where you never have to create a new document explicitly. Even when this option is true, you should still handle the CloseDocument correctly by clearing document UI, since creating the new document might fail. If this option is true, creating a new document should not require user interaction (such as selecting a document type or size).
ContainerControlThe control that contains the FileSelect instance. Normally, you do not need to change it. It is used for the following services:
  • If UpdateContainerTitle is set, the title of ContainerControl is adjusted to display the current document and the modified state.
  • It is used for Invoke for UI updates when the dirty state changes, so you can set the dirty state in a separate thread.
  • The controls VisibleChanged event is used for automatically creating an empty document when CloseCreatesNew is set.
DialogOpen, DialogSave:The file dialogs used to ask the user for a file name when opening or saving a document. You can customize their settings here. Note that some settings may be overwritten by FileSelect.
RecentFilesSettings for the recent files list.
 .ListCount(4) Lets you set the number of recent items displayed (ListCount). 
 .PersistCount (10) Number of recent files remembered. This can be larger than ListCount - why? When the user  notices the file you just wanted to open just dropped out of the recent files list, he might go to increase the number of files displayed there. The user now does not need to browse for the file, but has it in the recent list instantly. (You'd have to offer such a setting, though).
 .AddShortcuts (true) Adds numbered shortcut keys (1, 2, ...) to recent file lists.
 .DisplayLength (40) - If not -1, paths are shortened to the selected number of characters for recent file lists.
CustomUIStringsContains the strings used for end user display. You can customize and localize them here.

DocumentInfo

FileSelect holds a DocumentInfo for each document. It is passed to the handler events, and returned from various functions. It contains the following properties:

FilePathPath to the file the document was loaded from or saved to. May be null / empty, in when the user never specified a file name and will be asked for one when the document is saved.
CustomTitleA custom title set programmatically. It will be used e.g. for display in the container control title.
TitleThe current title of the document. Returns CustomTitle if one was set, otherwise the title is taken from the file, or a default name.
DirtyFlag Interface that handles document changes. When using the default implementation, you have to set the IsDirty property to true when the document changes.
IsDirtyA shortcut for DirtyFlag.IsDirty.

Programming API

HandleXxxxxDocumentProgrammatically trigger the respective commands.
OnXxxxxCan be overridden in a derived class, instead of implementing.
UIAskXxxxxxUser interactions - usually message boxes, can be overridden by a derived class.
SetCurrentDocument Provide a document that you have created or opened yourself as current. The function will return the document info, through which you can set attributes such as a custom document title and a file path. You have to update the user interface (the view of the document) manually.  Note that the user could cancel the operation (e.g. cancelling out of saving the current document). In this case, the function returns null.
RecentFiles.AllFiles Contains the list of recent files, separated by line breaks. You can persist this property in user settings, so the recent files list is remembered.

IDirtyFlag - Signalling Document Changes

The dirty flag affects which commands are enabled, and when message boxes are displayed, so for correct UI behavior, it needs to be implemented correctly.

Default Implementation: If you don't provide a custom implementation, you need to call  DocumentInfo.IsDirty = true (which is shorthand for DocumentInfo.DirtyFlag.IsDirty) anytime the document changes.

Custom Implementation: If your document class already provides a dirty state or other versioning mechanism, you can provide a custom event.

The public interface of IDirtyFlag is this:

C#
bool IsDirty { get; set; }
event EventHandler DirtyChanged;

FileSelect uses this interface to query the dirty state, modify the dirty state, e.g. after saving the document, or when NewDocumentIsDirty is enabled. It also binds to the change event to trigger UI updates. You can provide your custom implementation in two places:

  • Implement IDirtyFlag on your document class that you pass to e.g. FileSelectEventArgs.InitDocument.
  • Implement it on a standalone object, and pass it together with the document, e.g. to FileSelectEventArgs.InitDocument.

Important: Handling dirty state changes can be expensive, so the event should only fire when the state actually changes, not every time a value is assigned.

Architecture

Only an overview of the entities involved and their roles. If you have specific questions, please ask!

  • FileSelect contains the core implementation and provides the interaction with Visual Studio (designer properties, events, etc.)
  • DocumentInfo is an object associated with each document you open, containing document properties such as the current file name.
  • EFSCommand is an enumeration containing the available menu commands.
  • FileSelectEventArgs is the event class that is sent with FileSelect events.
  • RecentFileList implements the MRU cache for file names.
    RecentFiles inherits from RecentFileList and adds some designer properties used by FileSelect..
  • ICommandItem is the interface required for an adapter class wrapping menu or toolbar items, CommandItemBase provides some defaults for implementing it.
  • CI_ToolStripItem is the ICommandItem implementation used for WinForms tool strips (covering tool bars and menus).

Future Plans

Please take note El Corazon's (previous) signature of mice and ceilings.

This is only a first release, with some rough edges and quite some features to be desired.

Multiple Document Support would be a major enhancement, but since this is rather uncommon and I have no immediate application for that, I also have no plans for adding that anytime soon. However, I've made some considerations so this should be possible. Multiple single documents (each in its own top level window) can be supported by giving each top level form its own instance of FileSelect, though this leaves a few things to be desired.

Support for multiple document types. When opening or saving a document all you have to distinguish between different formats currently is the extension of the file, and the FilterIndex property of the file dialogs. This may be enough, but could be handled better. I've originally designed a document manager class that handles creating opening and saving the documents and represents different document types, but I've cut them from this release to reduce complexity. This also conflicts with the way I am using events where an interface or delegate would be more appropriate (but less convenient).

Automatically add application settings. I'd like to make that an option (e.g. a property "Add user settings automatically", and a prefix for the property names), but I haven't found a way to do that. Now, you need to add the properties to the user settings manually.

Motivation

I want to share some thoughts that are not directly related to using that control. Why did I write that? Surely, this is a spare time project and way too much time went into it than could ever be justified for commercial development.

First, it is a common pattern, and I get annoyed when I see something not only I have to do over and over, but also everyone else. When learning Windows Forms, I was missing some simplicity here. I did not really (read: "totally not at all") miss the Document / View - Architecture of MFC, as it was too inflexible and stubborn for my taste - you could use either all of it, or none, or you were in for a rough ride. That's one reason this component only handles the "UI side" of providing a document-centric application.

Second, I believe in what Jan Minkovsky describes as "fractal nature of UI design [^]": on his blog he describes the odyssey of remembering the window position. Seemingly simple, Every time he believed it was solved, a new complaint sprung up. What amazed me most is that I went through almost the same ordeal, though I solved some aspects differently. The way I'd describe it is this:

Every problem - small or large - fills the space it has available.

Whenever you have fixed the ugly glaring problem, another smaller issue will fill the gap and annoy the user as much as before. (There are two factors working against it: the user getting over it, and less users being affected by the new problem - but they set in surprisingly late).

Couple that with the quip that the best user interface is no user interface - in the sense that users should not notice the user interface at all, it should be transparent to them. We achieve that through various means - like real life metaphors, consistent user interface and metaphors across applications. However, that way we are creating the vacuum that any tiny annoyance can fill.

This is where simple to use and simple to implement diverge. Simple to use describes a state where the application does what the user expects, and our expectations are often amazingly complex. How come? I don't know.

History

  • Version 1.2 May 3, 2009
    • Breaking change: Default value for AskCloseUnmodified changed to false
    • Breaking change: UpdateParentTitleControl replaced by ContainerControl.and UpdateContainerTitle flag
    • Added Options AllowSaveUnmodified, CloseCreatesNew
    • Added FileSelect.SetCurrentDocument
    • Checking document object for IDirtyFlag implementation
    • Handling of "Open" command when current document is dirty similar to Notepad (document close after file was selected)
    • Several minor fixes to handling, code cleanups
    • Added Toolstrip and property grid with FileSelect settings
  • Version 1.1 April 5, 2009
    • Breaking changes: More consistent naming, some stricter requirements / validations, some signatures have been modified expecting more / less arguments.
    • Added shortening file paths for "Recent" Menu
      There are two options controlling the default behavior, and you can specify custom behavior through a delegate
    • RecentFilesList can now be modified programmatically
    • Separate command IDs for Recent as place holder and _RecentFile for the items created by it.
    • Added documentation
    • Added Sandcastle Builder project + CHM documentation
    • Fixed a few bugs
    • Note: Some literal strings still need to be moved to the UIStrings class
  • Version 1.0: March 29, 2009

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Klippel
Germany Germany
Peter is tired of being called "Mr. Chen", even so certain individuals insist on it. No, he's not chinese.

Peter has seen lots of boxes you youngsters wouldn't even accept as calculators. He is proud of having visited the insides of a 16 Bit Machine.

In his spare time he ponders new ways of turning groceries into biohazards, or tries to coax South American officials to add some stamps to his passport.

Beyond these trivialities Peter works for Klippel[^], a small german company that wants to make mankind happier by selling them novel loudspeaker measurement equipment.


Where are you from?[^]



Please, if you are using one of my articles for anything, just leave me a comment. Seeing that this stuff is actually useful to someone is what keeps me posting and updating them.
Should you happen to not like it, tell me, too

Comments and Discussions

 
GeneralMy vote of 1 Pin
Deepak Nigam19-Apr-10 21:26
Deepak Nigam19-Apr-10 21:26 
GeneralRe: My vote of 1 Pin
peterchen18-Aug-10 13:31
peterchen18-Aug-10 13:31 
GeneralRecent File List - Name ellipses (...) Pin
andre1234530-Mar-09 2:47
andre1234530-Mar-09 2:47 
GeneralRe: Recent File List - Name ellipses (...) Pin
peterchen30-Mar-09 3:11
peterchen30-Mar-09 3:11 
GeneralRe: Recent File List - Name ellipses (...) Pin
peterchen1-Apr-09 2:08
peterchen1-Apr-09 2:08 
GeneralTypo in article name Pin
ABitSmart29-Mar-09 22:35
ABitSmart29-Mar-09 22:35 
Generalfixed Pin
peterchen29-Mar-09 22:54
peterchen29-Mar-09 22:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.