Click here to Skip to main content
15,886,069 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 51.2K   512   39  
A WinForms user control that implements the details of file handling commands for any document-centric application
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO;


// http://msdn.microsoft.com/en-us/library/ms171724.aspx
// http://msdn.microsoft.com/en-us/library/6hws6h2t.aspx

/*
 * TODO:
 *   
 *   - UpdateParentTitle when DirtyState *changes*
 *   - I am still unhappy with that dirty handler
 *   - Recent files - Open, Save etc. add to recent files list
 *   - Recent files - settings
 *   - Recent files - build sub menu from it
 *   - Recent files - doc
 *   - Recent files - tests
 *   - Quit function for form close handler
 *   - Strings for FileDialog title
 *   - reset dirty for loaded documents after UI update?
 *   - file masks from doc type manager?? (or remove doc type manager?)
 *   - Document Managers for Serializable, XmlSerializable
 *   - replace "object or DocumentInfo" with "DocumentInfo", provide static DocumentInfo creator(s)
 *   - Localize strings
 *   - Parent window handle for message boxes
 *   - Implement "new Document dirty?"
 *   - "Shutdown" handler (similar to quit)
 *   
 *   - (?) suppress Close for "never modified" document?
 *   - (?) avoid frequent update of document title
 *   
 *   - (test!) automatic update of Form Title in all cases (except custom, SetDirty?!)
 *   - (test!) Implement event for ParentTitleChanged 
 *   
 *   - (ok) "Dirty change" notification ==> Handler should call RefreshTitle instead
 *   - (ok) implement AskCloseUnmodified
 *   - (ok) AskClose flag ==> rename to AskCloseUnmodified 
 *   - (ok) file dialog settings 
 *   
 *   - (no) customizable parent document title - easier to implement using event
 *   
 */
/* Features not supported yet
*  
*  - Document type specific file filters
*  - Save As Conversions (except by inspecting extension)
*  - Ask for naming a new document?
*  - support for managing document types
*  - separation of overrideable functions and "UI Helper" function is not clear
*  - storing the document manager selected with the document
*  - flag "Keep file handle open while document is opened"? (to deny concurrent access)
*  - Determining the current document for Save As etc. operations
*  - Standard "List of documents" dialog
* 
*  - Title for the message boxes - would be nice to pass the "reason for the action"
* 
* ------------------------
* 
* "Close" Behavior:
*    Closing an unsaved document:
*       (Option: Ask "Save? Yes / No Cancel" | Save named documents silently?) - hmm, maybe later
*       Option: "return to dialog when canceling save as"
*    Closing a saved document:
*      - "Close silently"
*      - Ask for close"
* 
* ------------------
*/



namespace PH.UI.FileSelect
{

  /// <summary>WinForms component implementing standard file commands.</summary>
  /// <remarks>
  /// <para>The control can be added to a WinForms form, and adds a FileSelectCommand 
  /// extender property to menu items. This lets you assign menu items to commands
  /// handled by the control. </para>
  /// <para>
  /// You also need to assign an <see cref="DocumentManager"/> that controls how to create,
  /// load and save a document. There are several default implementations. 
  /// </para>
  /// <para>
  /// The following functionality is provided by the FileSelect component:
  /// <list type="bullet">
  /// <item>The commands New, Open, Close, Save, Save As and Save Copy As</item>
  /// <item>Asking for file name when appropriate</item>
  /// <item>Handling modified / unmodified document</item>
  /// <item>Incremental default document title for new documents, e.g. Unknown-1, Unknown-2</item>
  /// <item>Setting form title to include document title and modification marker</item>
  /// </list>
  /// Typically, you need to provide only the following:
  /// <list type="bullet">
  /// <item>specify a default document manager, or providing your own IDocumentManager implementation</item>
  /// <item>hook up menu and toolbar items with FileSelect commands</item>
  /// <item>Handle the OpenDocument and OpenNewDocument events to display the document</item>
  /// <item>Handle the BeforeSaveDocument event to update the document from the user interface controls</item>
  /// <item>Handle the CloseDocument event to update the user display</item>
  /// <item>Signal that the document has been modified, or provide your own <see cref="IDirtyFlag"/> implementation</item>
  /// </list>
  /// </para>
  /// </remarks>
  [ProvideProperty("FileSelectCommand", typeof(object))]
  public partial class FileSelect : Component, IExtenderProvider
  {
    public FileSelect() : this(null) {}

    public FileSelect(IContainer container)
    {
      if (container != null)
        container.Add(this);
      InitializeComponent();
      Commands = new CommandMap(this);
      RecentFiles.FileListChanged += new EventHandler(RecentFiles_FileListChanged);

    }

    void RecentFiles_FileListChanged(object sender, EventArgs e)
    {
      List<string> filesList= new List<String>(RecentFiles.Files);
      string[] files = filesList.ToArray();

      foreach(IMenuItem imi in Commands.GetAllItemsFor(EFSCommand.RecentMore))
        imi.UpdateFileList(files, files);
    }

    #region Configuration Properties

    private bool m_singleFileMode = true;

    /// <summary>Switch between Single- and Multi-file Mode.</summary>
    /// <remarks>Currently, only single file mode is implemented.</remarks>
    [DefaultValue(true)]
    [Description("In single file mode, only a current document is supported. Otherwise, multiple documents are supported.")]
    public bool SingleFileMode
    {
      get { return m_singleFileMode; }
      // set { m_singleFileMode = value; }
    }

    private bool m_askCloseUnmodified = true;

    /// <summary>When a document is closed, ask the user if it should be saved.</summary>
    /// <remarks>When a document was modified, and the user closes the document, he will be asked 
    /// if he wants to save the document. The default is true. If this is set to false, 
    /// <see cref="AutoSaveOnClose"/> controls behavior.</remarks>
    [DefaultValue(true)]
    [Description("Ask if the user wants to save the current document before discarding changes.")]
    public bool AskCloseUnmodified
    {
      get { return m_askCloseUnmodified; }
      set { m_askCloseUnmodified = value; }
    }

    /// <summary>Control to hold the current document title.</summary>
    /// <remarks>Typically, you select the form containing the document editor. 
    /// The current document title and a "modified" marker is appended to the title 
    /// of the control. By default, this is <c>null</c>, which disables this feature.</remarks>
    Control m_parentControl = null;
    [DefaultValue(null)]
    public Control ParentControl
    {
      get { return m_parentControl; }
      set { m_parentControl = value; }
    }

    private bool m_bUpdateParentTitle = true;
    public bool UpdateParentTitle
    {
      get { return m_bUpdateParentTitle; }
      set 
      {
        if (value != m_bUpdateParentTitle)
        {
          m_bUpdateParentTitle = value;
          if (m_baseParentTitle != null)
            RefreshParentTitle(false);

        }
      }
    }

    private OpenFileDialog m_openFileDialog = new OpenFileDialog();
    private SaveFileDialog m_saveFileDialog = new SaveFileDialog();

    /// <summary>The dialog used in the default Open implementation.</summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public OpenFileDialog DialogOpen
    {
      get { return m_openFileDialog; }
    }

    /// <summary>The dialog used in the default Save implementations.</summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public SaveFileDialog DialogSave
    {
      get { return m_saveFileDialog; }
    }

    public class Strings : Component
    {
      string m_messageBoxTitle = "File";
      string m_askSaveChanges = "Save changes to this document?";
      string m_askCloseDocument = "Close document?";

      [Localizable(true)]
      [Description("Title for Message Boxes")]
      [DefaultValue("File")]
      public string MessageBoxTitle
      {
        get { return m_messageBoxTitle; }
        set { m_messageBoxTitle = value; }
      }

      [Localizable(true)]
      [Description("Message Box text: ask to save a modified file")]
      [DefaultValue("Save changes to this document?")]
      public string AskSaveChanges
      {
        get { return m_askSaveChanges; }
        set { m_askSaveChanges = value; }
      }

      [Localizable(true)]
      [Description("Message Box text: close unmodified document")]
      [DefaultValue("Close document?")]
      public string AskCloseDocument
      {
        get { return m_askCloseDocument; }
        set { m_askCloseDocument = value; }
      }
    }

    private Strings m_uiStrings = new Strings();
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public Strings UIStrings
    {
      get { return m_uiStrings; }
      //set { m_uiStrings = value; }
    }

    private RecentFileList m_recentFiles = new RecentFileList();

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public PH.UI.FileSelect.RecentFileList RecentFiles
    {
      get { return m_recentFiles; }
    }


    #endregion

    #region Programming Interface

    private IDocumentManager m_documentManager = null;

    /// <summary>The document manager to create, load and save documents.</summary>
    /// <remarks>...</remarks>
    [Browsable(false)]
    public IDocumentManager DocumentManager
    {
      get { return m_documentManager; }
      set { m_documentManager = value; }
    }

    // TODO: multiple documents support

    /// <summary>Additional information for the currently opened document.</summary>
    private DocumentInfo m_currentDocumentInfo = null;
    [Browsable(false)]
    public DocumentInfo CurrentDocumentInfo 
    {
      get { return m_currentDocumentInfo;  }
    }

    /// <summary>The currently opened document.</summary>
    [Browsable(false)]
    public object CurrentDocument
    {
      get 
      {
        if (CurrentDocumentInfo == null)
          return null;
        return CurrentDocumentInfo.Document;
      }
    }

    internal void SetCurrentDocumentInfo(DocumentInfo docInfo)
    {
      // TODO: verify we can just "throw away", support multiple documents, etc.
      m_currentDocumentInfo = docInfo;
      // note: RefreshParentTitle doesn't get called here, since it might be followed by a later update
    }


    private string m_baseParentTitle = null;
    public void RefreshParentTitle(bool sendEvent)
    {
      if (sendEvent)
        OnTitleBarChanged();

      if (m_parentControl == null)
        return;

      string title = null; 

      // restore previous title if it is known
      if (!m_bUpdateParentTitle && m_baseParentTitle != null)
      {
        title = m_baseParentTitle;
      }
      else
      {
        // get base parent control title
        if (m_baseParentTitle == null)
          m_baseParentTitle = m_parentControl.Text;

        if (CurrentDocumentInfo != null)
        {
          if (CurrentDocumentInfo.Path != null)
            title = System.IO.Path.GetFileName(CurrentDocumentInfo.Path);
          else
            title = CurrentDocumentInfo.Title;

          if (CurrentDocumentInfo.IsDirty)
            title += " (*)";

          if (m_baseParentTitle != String.Empty)
            title = title + " - " + m_baseParentTitle;
        }
        else
          title = m_baseParentTitle;
      }
      m_parentControl.Text = title;
    }

    public bool Quit(CloseReason closeReason)
    {
      if (closeReason == CloseReason.WindowsShutDown)
      {
        // TODO: provide custom handling
      }
      if (CurrentDocument != null)  // TODO: multi document support
      {
        bool closed = CloseDocument(CurrentDocumentInfo);
        if (!closed)
          return false;
      }
      return true;
    }


    #endregion

    #region Events and virtual functions

    /// <summary>fires when a new document was created.</summary>
    /// <remarks><para>You should handle this event by refreshing the user interface to reflect the new document contents.
    /// Alternatively, a derived class can override <see cref="OnOpenedNewDocument"/>.</para>
    /// <para>Usually, <c>OpenedNewDocument</c> and <see cref="OpenedDocument"/> can be handled by the same implementation.</para>
    /// <para>The Event Handler receives an instance of <see cref="FileSelectEventArgs"/> as event object.</para>
    /// </remarks>
    public event EventHandler OpenedNewDocument;


    protected virtual void OnOpenedNewDocument(DocumentInfo docInfo)
    {
      if (OpenedNewDocument != null)
        OpenedNewDocument(this, new FileSelectEventArgs(EFSCommand.New, docInfo));
    }

    /// <summary>fires when a document was loaded.</summary>
    /// <remarks><para>You should handle this event by refreshing the user interface to reflect the document contents.
    /// Alternatively, a derived class can override <see cref="OnOpenedDocument"/>.</para>
    /// <para>Usually, <see cref="OpenedNewDocument"/> and <c>OpenedDocument</c>> can be handled by the same implementation.</para>
    /// <para>The Event Handler receives an instance of <see cref="FileSelectEventArgs"/> as event object.</para>
    /// </remarks>
    public event EventHandler OpenedDocument;
    protected virtual void OnOpenedDocument(DocumentInfo docInfo)
    {
      if (OpenedDocument != null)
        OpenedDocument(this, new FileSelectEventArgs(EFSCommand.Open, docInfo));
    }

    /// <summary>fires before a document is saved.</summary>
    /// <remarks><para>You should handle this event by refreshing the document contents with the changes made in the user interface. 
    /// If the document is updated every time the user controls change, you need not handle this event.
    /// Alternatively, a derived class can override <see cref="OnBeforeSaveDocument"/>.</para>
    /// <para>The Event Handler receives an instance of <see cref="FileSelectEventArgs"/> as event object.</para>
    /// </remarks>
    public event EventHandler BeforeSaveDocument;
    protected virtual bool OnBeforeSaveDocument(DocumentInfo docInfo, string savePath, bool resetDirty)
    {
      if (BeforeSaveDocument != null)
      {
        FileSelectEventArgs fse = new FileSelectEventArgs(EFSCommand.BeforeSave, docInfo, savePath, resetDirty);
        BeforeSaveDocument(this, fse);
        return fse.ContinueSave;
      }
      else
        return true;

    }

    /// <summary>Fires after the document was saved.</summary>
    /// <remarks>You normally need not handle this event.     
    /// Alternatively, a derived class can override <see cref="OnSavedDocument"/>.
    /// </remarks>
    public event EventHandler SavedDocument;
    protected virtual void OnSavedDocument(EFSCommand cmd, DocumentInfo docInfo, string toPath, bool resetDirty)
    {
      if (SavedDocument != null)
        SavedDocument(this, new FileSelectEventArgs(cmd, docInfo, toPath, resetDirty));
    }

    /// <summary>fires when the document was closed.</summary>
    /// <remarks><para>You should handle this event by updating the user interface to indicate no document is open 
    /// Alternatively, a derived class can override <see cref="OnBeforeSaveDocument"/>.</para>
    /// </remarks>
    public event EventHandler ClosedDocument;
    protected virtual void OnClosedDocument(DocumentInfo docInfo)
    {
      if (ClosedDocument != null)
        ClosedDocument(this, new FileSelectEventArgs(EFSCommand.Close, docInfo));
    }

    public event EventHandler TitleBarChanged;
    protected virtual void OnTitleBarChanged()
    {
      if (TitleBarChanged != null)
      {
        TitleBarChanged(this, new FileSelectEventArgs(EFSCommand.ParentTitleChanged, CurrentDocumentInfo));
      }
    }

    #endregion

    #region Implementation Command Map

    /// <summary>utility class to implement <see cref="Commands"/>  member.</summary>
    /// <remarks>This class allows the caller to assign an <see cref="EFSCommand"/> to 
    /// one or more <see cref="IMenuItem"/>'s, which may e.g. be <c>ToolStripMenuItem</c>'s. </remarks>
    public class CommandMap
    {
      public readonly FileSelect Owner;

      public CommandMap(FileSelect owner)
      {
        Owner = owner;
        m_events = new EventForwarderList(Owner);
      }


      /*
      #region Event forwarder

      private class EventForwarderList
      {
        /// <summary>Helepr class to wrap and forward MenuItem standard events</summary>
        /// <remarks>FileSelect uses one instance per EFSCommand value, so that the forwarder can 
        /// attach the command Id t other forwarded event.</remarks>
        public class EventForwarder
        {
          public readonly EFSCommand Command;
          public readonly FileSelect Owner;

          public EventForwarder(EFSCommand cmd, FileSelect owner)
          {
            Command = cmd;
            Owner = owner;
          }

          public void ForwardEvent(object sender, EventArgs ea)
          {
            Owner.HandleMenuCommand(new MenuItemEventArgs(sender, ea, Command));
          }
        }

        private EventForwarder[] m_efl;

        public EventForwarderList(FileSelect owner)
        {
          m_efl = new EventForwarder[(int)EFSCommand._internal_Max];
          for (int i = 0; i < (int)EFSCommand._internal_Max; ++i)
            m_efl[i] = new EventForwarder((EFSCommand)i, owner);
        }

        public EventForwarder this[EFSCommand cmd]
        {
          get { return m_efl[(int)cmd]; }
        }
      }

      private EventForwarderList m_events;

      #endregion
      */

      private Dictionary<IMenuItem, EFSCommand> m_map = new Dictionary<IMenuItem, EFSCommand>();

      public EFSCommand this[IMenuItem item]
      {
        get
        {
          EFSCommand cmd;
          if (m_map.TryGetValue(item, out cmd))
            return cmd;
          else
            return EFSCommand.None;
        }
        set
        {
          EFSCommand prevCmd;
          if (m_map.TryGetValue(item, out prevCmd))
            item.Re;

          if (value == EFSCommand.None)
            m_map.Remove(item);
          else
          {
            m_map[item] = value;
            item.Click += m_events[value].ForwardEvent;
          }
        }
      }

      public IEnumerable<IMenuItem> GetAllItemsFor(EFSCommand cmd)
      {
        foreach (KeyValuePair<IMenuItem, EFSCommand> kvp in m_map)
        {
          if (kvp.Value == cmd)
            yield return kvp.Key;
        }
      }

      public IEnumerable<IMenuItem> GetAllItems()
      {
        return m_map.Keys;
      }


    }

    /// <summary>Assigns menu items etc. to commands.</summary>
    public CommandMap Commands;
    #endregion

    #region Utilities

    /// <summary>List of commands available</summary>
    public static EFSCommand[] EFSCommands = new EFSCommand[]
      {
        EFSCommand.New,         
        EFSCommand.NewMulti,    
        EFSCommand.Open,        
        EFSCommand.Close,       
        EFSCommand.Save,        
        EFSCommand.SaveAs,      
        EFSCommand.SaveCopyAs,  
        EFSCommand.Recent,      
        EFSCommand.RecentMore   
      };

    internal virtual bool CloseSingleDocument()
    {
      if (!m_singleFileMode)
        return true;

      if (CurrentDocumentInfo == null)
        return true;

      return CloseDocument(CurrentDocumentInfo);
    }

    #endregion

    #region Logic for handling commands

    internal virtual bool SaveDocument(DocumentInfo docInfo, EFSCommand cmd)
    {
      // TODO: for multiple documents, use document manager of current document!

      Debug.Assert(cmd == EFSCommand.Save || cmd == EFSCommand.SaveAs || cmd == EFSCommand.SaveCopyAs);

      bool askForName = true;
      string saveName = docInfo.Path;

      if (cmd == EFSCommand.Save)
      {
        saveName = docInfo.Path;
        askForName = String.IsNullOrEmpty(saveName);
      }
      else
      {
        // TODO: better default name for document?

      }

      if (askForName)
      {
        if (!UIAskForSaveName(cmd, ref saveName))
          return false;
      }

      bool resetDirty = cmd == EFSCommand.Save || cmd == EFSCommand.SaveAs;

      // call BeforeSafe handler
      bool continueSave = OnBeforeSaveDocument(docInfo, saveName, resetDirty);
      if (!continueSave)
        return false;


      // actually safe document
      DocumentManager.SaveDocument(docInfo.Document, saveName, resetDirty);

      // reset dirty flag using DirtyManager
      if (resetDirty)
        docInfo.IsDirty = false;

      // update document path
      if ((cmd == EFSCommand.Save && askForName) || cmd == EFSCommand.SaveAs)
        docInfo.Path = saveName;

      // update list of files
      RecentFiles.Add(docInfo.Path);

      // call after save handler
      OnSavedDocument(cmd, docInfo, saveName, resetDirty);

      if (resetDirty)
      {
        RefreshParentTitle(true);
        Debug.Assert(!docInfo.IsDirty, "FileSelect: document still dirty after saving");
      }
      return true;
    }

    internal virtual bool CloseDocument(DocumentInfo doc)
    {
      Debug.Assert(doc != null);

      // document dirty?
      if (doc.IsDirty)
      {

        bool doSave;
        //if (m_automaticSaveOnClose && doc.HasPath) // automatically save when path is known
        //  doSave = true;
        //else
        {
          // TODO: get / build message
          DialogResult result = UIAskSaveBeforeClose();
          if (result == DialogResult.Cancel)
            return false;

          doSave = result != DialogResult.No;
        }

        if (doSave)
        {
          if (!SaveDocument(doc, EFSCommand.Save))
          {
            return false;
            // TODO: add option to return to the "yes/no/cancel" dialog
            //  I've seen many people stuck there, so...  maybe as a configuration setting
          }
        }

      }
      else // document not dirty
      {
        if (m_askCloseUnmodified)
        {
          bool doClose = UIAskCloseUnmodified();
          if (!doClose)
            return false;
        }
      }

      // TODO: remove from multiple documents list
      if (m_singleFileMode)
      {
        Debug.Assert(doc == CurrentDocumentInfo);
        SetCurrentDocumentInfo(null);
      }
      OnClosedDocument(doc);
      RefreshParentTitle(true);
      return true;
    }

    internal virtual bool NewDocument()
    {
      if (!CloseSingleDocument())
        return false;

      RefreshParentTitle(true);  // TODO: refresh only once (first refresh is to ensure valid state when new document is canceled or fails)

      object newDocument = DocumentManager.NewDocument();
      if (newDocument == null)
        return false;

      // check if doc mgr already provided a document info
      DocumentInfo docInfo = newDocument as DocumentInfo;
      if (docInfo == null)  // - no? create it ourselves
      {
        docInfo = new DocumentInfo(newDocument, DocumentManager);
      }

      SetCurrentDocumentInfo(docInfo); // TODO: add to multi-doc-list

      OnOpenedNewDocument(docInfo);
      RefreshParentTitle(true);
      return true;
    }

    internal virtual bool OpenDocument()
    {
      if (!CloseSingleDocument())
        return false;

      string path = String.Empty;
      // TODO: better default path?

      if (!UIAskForOpenName(ref path))
        return false;

      object newDocument = DocumentManager.LoadDocument(path);
      if (newDocument == null)
        return false;

      // check if docmgr already returned a document info
      DocumentInfo docInfo = newDocument as DocumentInfo;
      if (docInfo == null) // create a new one otherwise
      {
        docInfo = new DocumentInfo(newDocument, DocumentManager);
        docInfo.Path = path;
      }

      SetCurrentDocumentInfo(docInfo); // TODO: add to multi-list
      RecentFiles.Add(docInfo.Path);
      OnOpenedDocument(docInfo);
      RefreshParentTitle(true);
      return true;
    }

    internal virtual void HandleMenuCommand(MenuItemEventArgs ea)
    {
      switch (ea.Command)
      {
        case EFSCommand.New:
          NewDocument();
          break;

        case EFSCommand.Close:
          if (CurrentDocumentInfo != null)
            CloseDocument(CurrentDocumentInfo);
          break;

        case EFSCommand.Save:
          if (CurrentDocumentInfo != null)
          {
            SaveDocument(CurrentDocumentInfo, EFSCommand.Save);
          }
          break;

        case EFSCommand.SaveAs:
          if (CurrentDocumentInfo != null)
          {
            SaveDocument(CurrentDocumentInfo, EFSCommand.SaveAs);
          }
          break;

        case EFSCommand.SaveCopyAs:
          if (CurrentDocumentInfo != null)
          {
            SaveDocument(CurrentDocumentInfo, EFSCommand.SaveCopyAs);
          }
          break;

        case EFSCommand.Open:
            OpenDocument();
            break;


        // todo: implement commands
        default:
          Debug.Assert(false, "FileSelect - Unsupported command:  " + ea.Command.ToString());
          break;
      }

    }

    #endregion

    #region MenuItem Adapter for WinForms
    private class WrapTSMI : IMenuItem
    {

      private EFSCommand m_command;
      private FileSelect m_owner;

      public WrapTSMI(FileSelect owner, EFSCommand command, ToolStripMenuItem item)
      {
        if (m_owner == null)
          throw new NullReferenceException();

        m_owner = owner;
        m_command = command;
        Item = item;
        Item.Click += new EventHandler(Item_Click);
      }

      void Item_Click(object sender, EventArgs e)
      {
        if (Click == null)
          return;

        ToolStripItem item = sender as ToolStripItem;
        if (item != null && item.Tag is OpenFileTag)
        {
          // TODO remove MenuItemEventArgs with three params for HandleMenuCommand
          // TODO: add file name
          m_owner.HandleMenuCommand(new MenuItemEventArgs(sender, e, EFSCommand.Recent));
        }
        else
        {
          m_owner.HandleMenuCommand(new MenuItemEventArgs(sender, e, m_command));
        }
      }

      // TODO: wrappers for other menu types!?

      public readonly ToolStripMenuItem Item;

      #region IMenuItem Members

      public bool Enabled
      {
        get { return Item.Enabled; }
        set { Item.Enabled = value; }
      }

      public bool Contains(object o)
      {
        return o != null && Item == o;
      }

      private class OpenFileTag
      {
        public OpenFileTag(string fileName) { FileName = fileName;  }
        public readonly string FileName;
      }

      public void UpdateFileList(string[] files, string[] displayStrings)
      {
        ToolStrip parent = Item.GetCurrentParent();
        int startIndex = parent.Items.IndexOf(Item);
        int existing = 0;  // number of existing elements

        while (startIndex+existing < parent.Items.Count && (parent.Items[startIndex+existing].Tag is OpenFileTag))
          ++existing;
        if (existing == 0)
          existing = 1; // initially, the start item is not tagged.

        // add or remove dummy items, as needed
        for(int i=existing; i<files.Length; ++i)
          parent.Items.Insert(startIndex+1, new ToolStripMenuItem());
        for (int i = 0; i < existing - Math.Max(files.Length, 1); ++i)
          parent.Items.RemoveAt(startIndex + 1);

        bool showPlaceholders = files.Length != 0 || (Item.Site != null && Item.Site.DesignMode);

        // update all items
        for(int i=0; i<files.Length; ++i)
        {
          ToolStripItem it = parent.Items[startIndex + i];
          it.Text = displayStrings[i];
          it.Tag = new OpenFileTag(files[i]);

          it.Click -= Item_Click; // remove handler if it was previously attached
          it.Click += Item_Click;
        }

        // show/hide placeholder and follow-up separator
        Item.Visible = showPlaceholders;
        int nextIndex = startIndex + Math.Max(files.Length, 1);
        if (nextIndex < parent.Items.Count &&
            parent.Items[nextIndex] is ToolStripSeparator)
          parent.Items[nextIndex].Visible = showPlaceholders;
      }

      #endregion
    }

    /// <summary>creates an IMenuItem for <see cref="Commands"/> from a <c>ToolStripMenuItem</c></summary>
    public static IMenuItem Wrap(ToolStripMenuItem item) { return new WrapTSMI(item); }
    #endregion

    /// <summary>Sets the command associated with a <c>ToolStripMenuItem</c></summary>
    /// <param name="tsmi"></param>
    /// <param name="cmd"></param>
    public void SetCommand(ToolStripMenuItem tsmi, EFSCommand cmd)
    {
      IMenuItem existing = GetMenuItemOf(tsmi);
      if (existing != null)
        Commands[existing] = cmd;
      else
        Commands[Wrap(tsmi)] = cmd;
    }

    #region UI overridables
    protected virtual DialogResult UIAskSaveBeforeClose()
    {
      // TODO: use custom message string from base class
      // TODO: include document info - title and/or path
      return MessageBox.Show(
        UIStrings.AskSaveChanges, 
        UIStrings.MessageBoxTitle, 
        MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
    }

    protected virtual bool UIAskForSaveName(EFSCommand cmd, ref string name)
    {
      SaveFileDialog dlg = DialogSave;
      dlg.FileName = name;
      dlg.Title = cmd.ToString(); // TODO: need to use resources anyway

      DialogResult result = dlg.ShowDialog();
      if (result != DialogResult.OK)
        return false;

      name = dlg.FileName;
      return true;
    }

    protected virtual bool UIAskForOpenName(ref string name)
    {
      OpenFileDialog dlg = DialogOpen;
      dlg.FileName = name;
      dlg.Title = EFSCommand.Open.ToString(); // TODO: need to use resources anyway

      DialogResult result = dlg.ShowDialog();
      if (result != DialogResult.OK)
        return false;

      name = dlg.FileName;
      return true;
    }

    private bool UIAskCloseUnmodified()
    {
      return DialogResult.Yes == MessageBox.Show(
        UIStrings.AskCloseDocument,
        UIStrings.MessageBoxTitle, MessageBoxButtons.YesNo, MessageBoxIcon.Question);
    }


    #endregion

    #region IExtenderProvider Members

    #region IExtenderProvider and Getters / Setters

    bool IExtenderProvider.CanExtend(object extendee)
    {
      if (extendee is ToolStripMenuItem)
        return true;
      return false;
    }

    private IMenuItem GetMenuItemOf(object o)
    {
      foreach (IMenuItem mi in Commands.GetAllItems())
      {
        if (mi.Contains(o))
          return mi;
      }
      return null;
    }

    public void SetFileSelectCommand(object o, EFSCommand cmd)
    {
      ToolStripMenuItem tsi = o as ToolStripMenuItem;
      if (tsi != null)
        SetCommand(tsi, cmd);
    }

    public EFSCommand GetFileSelectCommand(object o)
    {
      IMenuItem mi = GetMenuItemOf(o);
      if (mi != null)
        return Commands[mi];
      else
        return EFSCommand.None;
    }
    #endregion

    #endregion
  }

  /// <summary>Event Data for <see cref="FileSelect"/></summary>
  public class FileSelectEventArgs : EventArgs
  {
    internal FileSelectEventArgs(EFSCommand command, DocumentInfo documentInfo, string path, bool resetDirty)
    {
      Command = command;
      DocumentInfo = documentInfo;
      Path = path;
      ResetDirty = resetDirty;

    }

    internal FileSelectEventArgs(EFSCommand command, DocumentInfo documentInfo)
      : this(command, documentInfo, null, false)
    { }

    /// <summary>Type of the event.</summary>
    public readonly EFSCommand Command;

    /// <summary>DocumentInfo for the document that caused the event</summary>
    public readonly DocumentInfo DocumentInfo;

    /// <summary>Path, used in Save and Open events.</summary>
    public readonly string Path;

    /// <summary>Dirty flag will be reset, used with Save and SaveAs events.</summary>
    public readonly bool ResetDirty;

    bool m_bContinueSave = true; 
    /// <summary>Used in BeforeSaveEvent.</summary>
    public bool ContinueSave
    {
      get { return m_bContinueSave; }
      set 
      {
        if (Command != EFSCommand.BeforeSave)
          throw new InvalidOperationException("Setting BeforeSaveResult is only valid in BeforeSaveDocument handler.");
        m_bContinueSave = value; 
      }
    }

  }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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