Introduction
I'm building a timetable application using WPF. The first part of the project involves building a management application for the database. I built the database management application also in WPF. The application turned out really nice in my opinion (considering this is the first version and that I don't have much artistic sense :)), and so I decided to post it here.
Background
I suppose you need a medium level knowledge of WPF in order to understand the code. You need to know some SQL (for the database), some WPF control templates (because I will not talk about the templates I'm using), and some data binding. I have an article talking about WPF control templates. If you want, you can check it out here.
Using the Code
Before getting to the presentation, I want to tell you that the archive that is attached contains 30 pages of documentation that can help you understand everything about the application. You can read about the structure of the timetable and how to build one, about the structure of the database, and you also have a full documentation of the application presented here. The archive also contains a .sql file that you can use to set up the database. All you need to do is run it.
Instead, I'm going to show you some screenshots in order to show you what the application does, and I'm also going to talk a bit about these screenshots.
The image above represents the main application window. You can see here that the window is split up in two sections. The left section contains a list with all the tables in the database, while the section on the right is empty. This section will be empty only in the beginning. As soon as you select a table, the right part will be populated by a user control that will allow you to edit the respective table. You can see this in the image below:
What these images don't show is that when you start the application, some animations run. The list of tables slides into view from the left, while the user control section slides into view from the right.
Also, every time you select a user control, another animation runs while the control loads the data. You can see a glimpse of this in the image below:
All the user controls in the application (there are 9 of them) are implemented very similarly. Because of this, I'm only going to show you two of them here. The rest look and work in the same way.
The image above shows the user control used to edit the subjects table. You can add, edit, delete, and save subjects by using the three buttons at the bottom of the user control.
The last user control I'm going to show you here is the group subjects user control. This control is used to bind subjects to groups. The control can be seen in the image below:
As you can see, the UI is very friendly. At the top, we have the group for which we want to edit the subjects. This can be selected from a combo box. Below this, we have two lists. The list on the left shows the subjects that are linked to the current group. The list on the right shows the remaining subjects. We can add and remove subjects from the current group by using the two buttons that are located between the two lists.
Voice Commands
Like the title says, everything you can do with the mouse and the keyboard in this application you can also do hands free. You do this by using voice commands. In fact, the only manual thing you need to do (have to do) is start the application. The rest you can do with a microphone (even close the application).
I'm doing this with the voice recognition API that comes with the .NET Framework. This voice recognition comes with Windows Vista. On Windows XP, you will have it only if you install Office XP with the speech recognition engine. You can select this option during installation. The speech recognition API is also a Windows accessibility feature, because it allows people with disabilities to operate applications on the PC.
I added this feature because I was reading about this in a book and I thought it will be fun to implement. I hope you like this feature even though it is noticeably slower than the mouse.
The most straightforward way to use speech recognition is to create an instance of the SpeechRecognizer
class from the System.Speech.Recognition
namespace. You can then attach an event handler to the SpeechRecognized
event, which is fired whenever spoken words are successfully converted to text:
SpeechRecognizer recognizer = new SpeechRecognizer();
recognizer.SpeechRecognized += recognizer_SpeechReconized;
You can then retrieve the text in the event handler from the SpeechRecognizedEventArgs.Result
property:
private void recognizer_SpeechReconized(object sender,
SpeechRecognizedEventArgs e)
{
MessageBox.Show("You said:" + e.Result.Text);
}
Another way to use speech recognition is to use a custom grammar. This is the approach I chose because I couldn't get the other one to work right. The grammar I'm using might not be the best, but it works well with the application. I'm building the grammar with the function below.
private Grammar GetAppGramar()
{
GrammarBuilder builder = new GrammarBuilder();
builder.Append(new Choices(" ","computer"));
builder.Append(new Choices(" ","select","details"));
builder.Append(new Choices(" ", "save", "new", "delete",
"add", "remove","remaining", "main"));
builder.Append(new Choices(" ","rooms", "groups", "subjects",
"professors","subject types", "room","professor","subject",
"semester","number of hours","semi group",
"specializations","specialization","bind subjects",
"bind professors","bind rooms"));
builder.Append(new Choices(" ", "name", "description",
"first name", "last name", "type", "code", "list"));
builder.Append(new Choices(" ", "next", "previous","first",
"last","open list","close list","exit application"));
return new Grammar(builder);
}
This grammar is then applied to the SpeechRecognizer
by using the LoadGrammar()
or LoadGrammarAsync()
functions. This can be seen in the code below:
speech = new SpeechRecognizer();
speech.LoadGrammarAsync(GetAppGramar());
I think that the grammar I built works by combining one choice from the first row with one from the second and so on, in order to recognize a command. This is why every row in the grammar building function contains a space character. I use it to recognize single word commands.
The rest of the interesting code is in the SpeechRecognized
handler. In the following sub sections, I'm going to tell you about the commands I'm using.
Selection in the main table list
In order to select the items in the table list (the list on the left of the user controls), I am using the following commands: "select specializations", "select subject types", "select subjects", "select groups", "select rooms", "select professors", "bind rooms", "bind subjects", and "bind professors". When I use one of these commands, I'm setting the selected index of the list, and this in turn goes on to change the user control on the right. Some of the code that does this can be seen below:
if (e.Result.Text.ToLower() == "select groups")
{
lstOptions.SelectedIndex = 3;
}
else if (e.Result.Text.ToLower() == "select rooms")
{
lstOptions.SelectedIndex = 5;
}
Adding elements, deleting elements, removing elements, and saving changes
I'm using five voice commands in order to edit most of the tables in the database. This is easily done because I'm using commands in the corresponding user controls. The five voice commands are: "add", "new", "remove", "delete", and "save". The code that handles this for the delete and remove voice commands can be seen below. The rest of the commands are implemented the same way.
else if (e.Result.Text.ToLower() == "delete")
{
if (currentControl != null && (currentControl is ProfsControl ||
currentControl is SubjectTypesControl ||
currentControl is RoomsControl || currentControl is SubjectsControl ||
currentControl is SpecializationsControl ||
currentControl is GroupsControl))
{
Button delBtn = (Button)currentControl.FindName("btnDelete");
if(delBtn!=null)
TimetableCommands.Delete.Execute(null, delBtn);
}
}
else if (e.Result.Text.ToLower() == "remove")
{
if (currentControl != null && (currentControl is RoomsSubjectsControl ||
currentControl is ProfsSubjectsControl ||
currentControl is GroupsSubjectsControl))
{
Button btn = (Button)currentControl.FindName("btnRemove");
if (btn != null)
TimetableCommands.Remove.Execute(null, btn);
}
}
As you can see, first of all, I'm retrieving the correct button for the current user control by using the FindName()
method. After this, I'm explicitly triggering the corresponding command, passing as the second argument, the element from where the bubbling should start. Because the user control has the command bindings, the result is the same as I had pressed the button with the mouse. This feature would not have been possible had I implemented the user controls with normal event handlers.
Selecting text boxes and other elements
The text box selection is also easy. All you need to do is say the text of the label that is in front of the text box. A code example can be seen below.
else if (e.Result.Text.ToLower() == "name")
{
if (currentControl != null)
{
TextBox txtName = (TextBox)currentControl.FindName("txtName");
if (txtName != null)
txtName.Focus();
}
}
else if (e.Result.Text.ToLower() == "description")
{
if (currentControl != null)
{
TextBox txtDesc = (TextBox)currentControl.FindName("txtDescription");
if (txtDesc != null)
txtDesc.Focus();
}
}
As you can see, the code first checks to see if we have an opened user control. If we do, it tries to retrieve the specified text box. If it finds the text box, it calls the Focus()
method on it in order to set the focus. You can also see that this code depends heavily on the names of the text boxes. The same principle applies when selecting list boxes or combo boxes in the user controls on the right. Some commands for this might be: "number of hours", "specialization", "room list", "remaining rooms", "professor list", and "remaining professors", to name a few.
Selecting the main list
The main list represents the list at the top of each user control. This can be a list box or a combo box. The selection of this main list can be done using the "select main list" voice command. The code checks the current user control, retrieves the list, and then sets the focus on that list. Some of the code that does this can be seen below.
ItemsControl list = null;
if(currentControl is ProfsControl)
{
list = (ItemsControl)currentControl.FindName("lstProfs");
}
else if(currentControl is GroupsControl)
{
list = (ItemsControl)currentControl.FindName("lstGroups");
}
Open and close the combo box popup
In the case of combo boxes, the user might want to open the popup before navigating. In order to do this, he can use the "open list" voice command. To close the popup, he can use the "close list" voice command. The code that does this can be seen below.
else if (e.Result.Text.ToLower() == "open list")
{
IInputElement el = FocusManager.GetFocusedElement(this);
if (el is ComboBox)
{
ComboBox cb = el as ComboBox;
cb.IsDropDownOpen = true;
}
}
else if (e.Result.Text.ToLower() == "close list")
{
IInputElement el = FocusManager.GetFocusedElement(this);
if (el is ComboBox)
{
ComboBox cb = el as ComboBox;
cb.IsDropDownOpen = false;
}
else if (el is ComboBoxItem)
{
ComboBoxItem cbi = el as ComboBoxItem;
Grid cbgr = FindDropDownGrid(cbi);
if (cbgr!=null)
{
if (cbgr.TemplatedParent != null)
{
ComboBox cb =
cbgr.TemplatedParent as ComboBox;
if (cb != null)
{
cb.IsDropDownOpen = false;
}
}
}
}
}
The popup is opened by setting the IsDropDownOpen
property. The interesting part of the code is in the "close list" command. When the popup is opened, the item with the focus is no longer the combo box but the current combo box item. In order to successfully use the "close list" voice command or the navigation commands (discussed in the next section), the combo box needs to have focus. In order to get a reference to the combo box, the code uses the FindDropDownGrid()
function to get a grid in the control template with a certain name. After this, the code can use the TemplatedParent
property of this grid to get this combo box. This can be done because this grid was created in a template. The function can be seen below:
private Grid FindDropDownGrid(DependencyObject child)
{
DependencyObject p = VisualTreeHelper.GetParent(child);
if (p == null)
return null;
if (p is Grid)
{
Grid gr = p as Grid;
if (gr.Name.ToLower().Equals("dropdown"))
return gr;
return FindDropDownGrid(p);
}
else
{
return FindDropDownGrid(p);
}
}
As you can see, the function uses the VisualTreeHelper
to find a parent called "dropdown". You must keep in mind that this is heavily dependent on the combo box control template I'm using.
Navigating lists and combo boxes
The last commands I'm going to talk about are the commands used to navigate the lists. I have four voice commands: "first", "next", "previous", and "last". The names are pretty self-explanatory. The navigation is done using a function called navigateMainList()
. This function takes as its only parameter a string that represents the command. First, the function retrieves the focused control. This control needs to be an ItemsControl
or a ComboBoxItem
in case of an opened combo box. The function retrieves the view of the collection, and based on the parameter it gets, it uses one of the ICollectionView
navigation methods. An example can be seen below.
ItemsControl itemsControl = el as ItemsControl;
ICollectionView view =
CollectionViewSource.GetDefaultView(itemsControl.DataContext);
if (option == "first")
{
view.MoveCurrentToFirst();
}
At the end, I call ScrollIntoView
to show the element. This is done with the following code:
if (itemsControl is ListBox)
{
ListBox lst = itemsControl as ListBox;
lst.ScrollIntoView(lst.SelectedItem);
}
In case the selected item is a ComboBoxItem
, the method does almost the same thing. The function first gets the corresponding combo box for the item using the FindGetDropDownGrid()
method mentioned previously. After this, it gets the combo box view and uses the ICollectionView
navigation functions.
To exit the application, you can use the "exit application" voice command.
This is it. Please feel free to download the application, and read the documentation to better understand what I did and how I did it. Also, feel free to post your comments and ideas about how the application can be improved (functionality, control templates, animations, etc.).
A glimpse into the timetable client application
Although, in my opinion, this isn't actually related to the article, I have decided to show some screenshots of the timetable client application. This application will use the database to build the actual timetables, and will also save them as XML files in order to be further edited at a later time.
The application creates projects that contain one or more timetable files. In order to give you a better understanding of how the application will work, I'm going to present a common operation scenario.
This scenario includes creating a timetable project and files, adding and editing entries, validating the timetable, and generating additional timetables. The image below presents the main window of the application:
This is an MDI application that presents the timetable documents in multiple tabs, giving the user the possibility to edit multiple timetables very fast. In order to create a new timetable project, the user will need to select the File->New->Project option from the main menu or click the New Project toolbar button. From the New Project dialog, the user will select the Timetable template, will specify a name for the project, and will choose the project location. This can be seen in the image below.
After the project has been created, the user can start adding timetable files by using the File->New->File menu option or by clicking the toolbar button. In the New File dialog, the user will select the name of the file, the specialization, and the semester. The image below shows the project after a few timetable files have been added and the documents have been opened.
As you can see, from the image above, the application presents a tree view of the project that contains the project files. It also presents a list of the subjects for the selected specialization. The main area of the application is reserved for presenting the timetable documents.
In order to add entries to a timetable, all the user needs to do is drag and drop items from the subjects list. If the subjects list is not visible, it can be shown from the View menu. After the user lets the entry on the timetable surface, a dialog appears asking for more information. In the Add New Entry dialog, the user can select the professor, room, and starting week type for the current entry. This can be seen in the image below:
The image below shows the timetable for Automatics specialization after a few entries have been added:
There are a few things you can notice here. First is that in some cases, not all the information can be displayed in the entries. If this is the case, you can use the tool tips to get more information about an entry. Another thing to notice is that the user can select entries and drag them over the surface of the timetable in order to change their position.
The last thing is the validation. In order to validate a timetable, the user can press F6 or can access the command from the menu. The image below presents a timetable after validation.
As you can see, the entries in error are marked with an orange color. Also, all the errors and warnings are listed in the error list. From here, the user can double click an error and be transported to the entry in question even if the file isn't currently opened. For each error, the application presents: a description of the error, the file that contains the entry, the day and period where the entry is positioned, the group and the semi group number.
History
- Added on Friday, January 29, 2010.