![]() |
Desktop Development »
Menus »
General
Beginner
License: The Code Project Open License (CPOL)
FileSelect - Hassle Free Implementation of the File MenuBy peterchenA WinForms user control that implements the details of file handling commands for any document-centric application |
C#2.0, C#3.0, Windows, .NET2.0, .NET3.0, .NET3.5, WinForms, VS2005, VS2008, Architect, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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:
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.
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
MenuStripcomponent, 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:
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
}
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:
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
SaveCompleteto true when saving the document was successful. If you don't,FileSelectassumes 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:
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;
}
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".
}
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). |
ContainerControl |
The control that contains the FileSelect instance. Normally, you do not need to change it. It is used for the following services:
|
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. |
RecentFiles |
Settings 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. |
CustomUIStrings |
Contains the strings used for end user display. You can customize and localize them here. |
FileSelect holds a DocumentInfo for each document. It is passed to the handler events, and returned from various functions. It contains the following properties:
FilePath |
Path 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. |
CustomTitle |
A custom title set programmatically. It will be used e.g. for display in the container control title. |
Title |
The 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. |
IsDirty |
A shortcut for DirtyFlag.IsDirty. |
HandleXxxxxDocument |
Programmatically trigger the respective commands. |
OnXxxxx |
Can be overridden in a derived class, instead of implementing. |
UIAskXxxxxx |
User 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. |
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:
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:
IDirtyFlag on your document class that you pass to e.g. FileSelectEventArgs.InitDocument.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.
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).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.
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.
AskCloseUnmodified changed to falseUpdateParentTitleControl replaced by ContainerControl.and UpdateContainerTitle flagAllowSaveUnmodified, CloseCreatesNewFileSelect.SetCurrentDocumentIDirtyFlag implementationFileSelect settingsRecentFilesList can now be modified programmatically_RecentFile for the items created by it. UIStrings class| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 4 May 2009 Editor: Deeksha Shenoy |
Copyright 2009 by peterchen Everything else Copyright © CodeProject, 1999-2010 Web22 | Advertise on the Code Project |