Creating a Most Recents Menu Item with the MenuStrip

Introduction

Patterns exist for a reason. Patterns are something we can get familiar with, consequently making like-things anticipatable and easier to use. I am not just talking about patterns like the Gang of Four GoF patterns; I mean simple ways that things work too.

Now, don't get me wrong. Re-inventing the wheel makes sense if you go from a rock wheel to a rubber one, but like things are easier to adopt when they are new if they are used like similar things that exist. For example, if I have a Menu and it has a collection of element-like sub-menus, I am going to look automatically for a collection property and an add method. Easy.

The MenuStrip is like a menu, but if you want to add a sub-menu it's not as easy as invoking Add with an instance of a menu item. It's sort of one off. In this article, I will show you how to dynamically add ToolStripMenuItems to a MenuStrip. If you know how to do that, I encourage you to read the article anyway. After the half—it is football season after all—the dynamic menu is added to a Command class, making the use of this technique a matter of importing the command class in any future application. It's also cooler and better housekeeping. If you already know how to do all of these things, I won't mind if you wait for the next article. ~wink~

Creating a Recent Submenu with the MenuStrip

Many applications have dynamic menus. Even Visual Studio tracks open files and projects in "recents" menus. The basic behavior is to open a file and create a dynamic sub-menu with the name of the file and add that menu to the MenuStrip. The code in Listing 1 is plain old vanilla VB.NET code behind a simple form with a File menu, an Open sub-menu, and a Recent Files sub-menu (see Figure 1). When you Open the file, it is read into a TextBox and a dynamic menu is created.

Figure 1: A simple text browser containing a multi-line TextBox and a MenuStrip.

Listing 1: Click Open, select a file, and create a dynamic sub-menu off of the Recent Files menu.

Imports System.IO
Imports System.Diagnostics


Public Class Form1

   Private Sub OpenToolStripMenuItem_Click(ByVal sender _
      As System.Object, _
      ByVal e As System.EventArgs) _
      Handles OpenToolStripMenuItem.Click
      OpenFileDialog1.InitialDirectory = "C:\TEMP"
      OpenFileDialog1.Filter = "Text Files (*.txt)|*.txt"
      OpenFileDialog1.Title = "Open File"
      OpenFileDialog1.ShowDialog()
   End Sub

   Private Function OpenFile(ByVal filename As String) As Integer
      Debug.Assert(File.Exists(filename))
      Dim data As String = File.ReadAllText(filename)
      TextBox1.Text = data.Replace("\r", " ")
      Return 0
   End Function

   Private Sub RecentsClick(ByVal sender As Object, _
      ByVal e As EventArgs)
      If (TypeOf sender Is ToolStripMenuItem = False) Then Return
      Dim menu As ToolStripMenuItem = CType(sender, _
         ToolStripMenuItem)
      OpenFile(menu.Text)
   End Sub

   Private Sub AddToRecents(ByVal filename As String)
      Dim menu As ToolStripMenuItem = _
         RecentFilesToolStripMenuItem.DropDownItems.Add(filename)
      AddHandler menu.Click, AddressOf RecentsClick
   End Sub

   Private Sub OpenFileDialog1_FileOk(ByVal sender As System.Object, _
   ByVal e As System.ComponentModel.CancelEventArgs) _
      Handles OpenFileDialog1.FileOk
      Dim dialog As OpenFileDialog = DirectCast(sender, OpenFileDialog)
      OpenFile(dialog.FileName)
      AddToRecents(dialog.FileName)
   End Sub

End Class

In the code, the OpenToolStripMenuItem_Click event responds when you click the File|Open menu. The click event sets some properties of an OpenFileDialog control and calls ShowDialog. You can check the return value of ShowDialog or handle FileOK in an event of the OpenFileDialog, which is show as the last method of Listing 1.

If the user clicks OK, OpenFileDialog1_FileOK runs. FileOK casts the sender using DirectCast to an OpenFileDialog object (rather than using the OpenFileDialog1 object directly), making the code more portable. The OpenFile method is called and AddToRecents is called.

OpenFile uses basic File I/O to read the text file with the shared method File.ReadAllText. String.Replace strips the carriage return characters. (You can skip that line if you like.) Finally, the text is put in the TextBox.

Note: By the way, when you write code this straightforward using named methods like OpenFile and AddToRecents, you don't need comments. Tell them I said it was OK.

AddToRecents adds the filename string as the text for the dynamic menu. Notice that you have to refer to the parent menu's DropDownItems property—not as intuitive as just Add—and call DropDownItems.Add method. Add returns the new ToolStripMenuItem, and you attach an event handler.

The last bit is the RecentsClick that responds when you click on the new dynamic menu. The code does a simple sanity check to see whether the sender is a ToolStripMenuItem. (You could safely skip that check here because your code is doing the wire-up of the event, so you know it's a ToolStripMenuItem. By the way, this form of the If conditional is called a sentinel; it's shorter and sweeter than an If Then End If, but does result in multiple exit points.) Finally, you reuse OpenFile, which is exactly why you use named methods rather than writing all code directly in the event handler itself.

Of course, you don't have to agree with me on style, but my style for me is a very fast way to program. It reduces superfluous comments, and promotes reuse.

Refactoring the Code into a Command Class

Now, in all but trivial samples I might start with code like Listing 1 (probably not, but I might). What I would end up with, though, is a Command class for the Open File with Recents behavior embedded in it. Why? Good question. Encapsulating this behavior in a class means it's portable at the class level, it keeps my Form code simpler, and it promotes reuse in other ways. For example, with a Command class I could add new ways to invoke the behavior like through a button, a wizard, auto-pilot mode, or perhaps as a controllable API (think OLE Automation-type control).

Listing 2 shows the Form code Refactored. The code, combined with Listing 3, does the same thing but now most of the behaviors are moved to an external command class.

Listing 2: Form1 revised to use a Command class.

Imports System.IO
Imports System.Diagnostics


Public Class Form1
   Private _openFileCommand As OpenFileCommand

   Private Sub OpenToolStripMenuItem_Click(ByVal sender _
      As System.Object, _
      ByVal e As System.EventArgs) Handles OpenToolStripMenuItem.Click
      _openFileCommand._Do()
   End Sub

   Private Function OpenFile(ByVal filename As String) As Integer
      Debug.Assert(File.Exists(filename))
      Dim data As String = File.ReadAllText(filename)
      TextBox1.Text = data.Replace("\r", " ")
      Return 0
   End Function

   Private Sub FileOK(ByVal sender As Object, ByVal e As FileEventArgs)
      OpenFile(e.FileName)
   End Sub

   Private Sub Form1_Load(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) _
      Handles MyBase.Load

      _openFileCommand = New OpenFileCommand()
      AddHandler _openFileCommand.FileOKClick, AddressOf FileOK
      AddHandler _openFileCommand.RecentsClicked, AddressOf FileOK
      _openFileCommand.RecentsMenu = RecentFilesToolStripMenuItem

   End Sub
End Class

The two things that happen in the new form are the initialization of the command object with event wire-ups and the OpenFile method and that is it. Listing 3 contains the command class.

Creating a Most Recents Menu Item with the MenuStrip

Listing 3: The OpenFileCommand now contains all of the orchestration among opening a file, signaling the Command class consumer, and populating the Recent Files menu.

Imports System.IO
Imports System.Diagnostics
Imports System.Windows.Forms

Public Class FileEventArgs
   Inherits EventArgs

   Public Sub New(filename As String)
      MyBase.New()
      _fileName = filename
   End Sub

   Private _fileName As String
   Public Property FileName() As String
      Get
         Return _fileName
      End Get
      Set(ByVal Value As String)
         _fileName = Value
      End Set
   End Property

End Class

Public Class OpenFileCommand

   Private _openFileDialog As OpenFileDialog

   Public Sub New()
      _openFileDialog = New OpenFileDialog()
      AddHandler _openFileDialog.FileOk, AddressOf FileOK

   End Sub

   Private Sub FileOK(ByVal sender As Object, ByVal e As EventArgs)
      RaiseEvent FileOKClick(Me, _
         New FileEventArgs(_openFileDialog.FileName))
      AddToRecents()
   End Sub

   Private Sub AddToRecents()
      If (_recentsMenu Is Nothing) Then Return
      Dim menu As ToolStripMenuItem = _
         _recentsMenu.DropDownItems.Add(_openFileDialog.FileName)
   AddHandler menu.Click, AddressOf RecentsClick
   End Sub

   Private Sub RecentsClick(ByVal sender As Object, ByVal e As EventArgs)
      RaiseEvent RecentsClicked(Me, _
         New FileEventArgs(DirectCast(sender, _
         ToolStripMenuItem).Text))
   End Sub

   Public Event RecentsClicked As EventHandler(Of FileEventArgs)
   Public Event FileOKClick As EventHandler(Of FileEventArgs)

   Private _initialDirectory As String = "C:\TEMP"
   Public Property InitialDirectory() As String
      Get
         Return _initialDirectory
      End Get
      Set(ByVal Value As String)
         _initialDirectory = Value
      End Set
   End Property

   Private _filter As String = "Text Files (*.txt)|*.txt"
   Public Property Filter() As String
      Get
         Return _filter
      End Get
      Set(ByVal Value As String)
         _filter = Value
      End Set
   End Property

   Private _title As String = "Open File"
   Public Property Title() As String
      Get
         Return _title
      End Get
      Set(ByVal Value As String)
         _title = Value
      End Set
   End Property

   Private _recentsMenu As ToolStripMenuItem
   Public Property RecentsMenu() As ToolStripMenuItem
      Get
         Return _recentsMenu
      End Get
      Set(ByVal Value As ToolStripMenuItem)
         _recentsMenu = Value
      End Set
   End Property

   Public Sub _Do()
      _openFileDialog.InitialDirectory = _initialDirectory
      _openFileDialog.Filter = _filter
      _openFileDialog.Title = _title
      _openFileDialog.ShowDialog()
   End Sub

   Public Sub Undo()

   End Sub

End Class

The FileEventArgs class in my new EventArgs is my new event argument for the OpenFileCommand. It simply contains a FileName property, representing the return value from the OpenFileDialog. The OpenFileCommand contains an OpenFileDialog field. The constructor (Sub New) creates an instance of the OpenFileDialog and wires up the FileOK event.

The FileOK event handler raises an event to let external consumers, in this case the form, handle FileOK and it calls AddToRecents. AddToRecents checks to see whether you have indicated what menu is the recents menu by using a sentinel and dyanmically adds the indicated filename as the menu item name.

RecentsClick raises an event to the external consumer (the form again). The next two fields are event fields. Consumers need to attach to these two events. The code for doing this is shown in Listing 2. The next three properties represent some stuff I want to let the consumer configure, such as Title and Filter. And, the fourth property lets the consumer indicate the ToolStripMenuItem that represents the Recent Files menu.

Do is the only public method making the OpenFileCommand pretty easy to use. Call Do and it initializes the OpenFileDialog and shows it. If the user clicks OK, the FileOK behavior takes over. Undo is there for symmetry and it's a traditional part of the Command behavior battern. Look at dofactory.com for a great elaboration on the Command pattern or check out my (much) earlier article on the Command pattern at Devx.com. (You can also learn how to incorporate patterns into all of your code by reading books like my recent book LINQ Unleashed for C#. I know you're VB programmers, but if you can program you can read C#.)

Summary

Elevating the quality of your product is knowing how to do relative simple things like adding dynamic menus and then encapsulating them so you don't have to do them again.

I recall about ten years ago doing some walkthroughs and people would argue about what is good code and what isn't. The subjectivity is somewhat mitigated now by this concept: good code has a high frequency and density of well-known patterns at least at a minimum. No patterns and...

About the Author

Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his upcoming book LINQ Unleashed for C# now available on Amazon.com and fine bookstores everywhere. You may contact him for technology questions at pkimmel@softconcepts.com.

Copyright © 2008 By Paul T. Kimmel. All Rights Reserved.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

  • Webinar on September 23, 2014, 2 p.m. ET / 11 a.m. PT Mobile commerce presents an array of opportunities for any business -- from connecting with your customers through mobile apps to enriching operations with mobile enterprise solutions. Join guest speaker, Michael Facemire, Forrester Research, Inc. Principal Analyst, as he discusses the new demands of mobile engagement and how application program interfaces (APIs) play a crucial role. Check out this upcoming webinar to learn about the new set of …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds