Extending Omea with New Resource Types
Viewing Resources
Now we have the possibility to create books but there is no way to see the books we've created. Two things are required for us to see the books: a view where we can see the list of books and the UserControl to display the actual book contents.
To keep things simple, the initial version of the sample plugin will only have one view to show the books - the "All Books" view. We will demonstrate how to register a tab where the view appears and how to register the view itself.
The tab is registered with one simple call:
Core.TabManager.RegisterResourceTypeTab( "Books", "Books", _resBook, 20 );
We specify the ID and the display name of the tab, the type of resources shown
in the tab and the index of the tab related to other tabs. The standard tabs,
except for Tasks, have indexes between 0 and 10, and the Tasks tab has the index
of 99. Thus, the index 20 places us after all the standard tabs but before
Tasks.
After we register a tab, we get the standard Views and Categories pane and not much else. Books by default don't have any properties which are used for filtering by standard views (like date or annotation), so the views will initially be empty. However, we can easily add the view to display the list of all books.
In order for the "Restore Default Views" feature to work correctly for the
plugin views, views should not be registered directly from the Register() or
Startup() method of the plugin. Instead, the plugin should contain an
implementation of the IViewsConstructor interface, register that implementation
in the Register() method, and perform the views registration.
If you start the plugin now, you will see that the "All Books" view appears in the "Books" tab, and the books you create are shown in that view, but the preview pane is empty when you select a book resource. In order for the resource to be displayed, we need to provide a display pane implementation.
To understand how to implement the display pane, we need to understand the
lifecycle of the display panes first. Each plugin which needs to display
resources registers an implementation of IResourceDisplayer class, which
basically serves as a factory for display panes. When Omea needs to display a
resource, it locates a plugin which has registered a resource displayer for the
required resource type and calls its CreateDisplayPane() method. The method
returns a class which implements the IDisplayPane interface. Omea then embeds
the control returned by IDisplayPane.GetControl() into the resource browser and
calls IDisplayPane.DisplayResource() to get the resource displayed.
As the user switches between the resources of the same type, Omea continues
to use the same display pane instance. It calls EndDisplayResource() before the
user switches to a different resource and then DisplayResource() again to get
the new resource displayed. If the user switches to a resource of a different
type, the display pane is disposed. This is done by calling EndDisplayResource()
for the last time, followed by a call to DisposePane(). The DisposePane()
implementation usually disposes of the UserControl which was used to display the
resource.
In our sample we will follow the common pattern and keep the
IResourceDisplayer implementation in the same class which implements IPlugin.
The IDisplayPane interface will be implemented by the user control which is used
to display the books.
The actual IDisplayPane implementation is pretty much trivial. The only
method that contains real code is
DisplayResource(). All the others would be
needed if we wanted to implement more complex functionality - for example,
selection and copy/paste, watching resource changes or highlighting search
results.
Making Resources Searchable
Making it possible for the user to search the plugin resources requires two main tasks from the plugin developer:
- Implementing and registering a class that will provide searchable text for book resources;
- Requesting text indexing for the resources when they are changed.
The main decisions that need to be made when implementing the text provider
are: which of the resource properties should be searchable, and in what sections
should the properties be divided. In our plugin, we want the user to be able to
find a book if they enter its name, author or ISBN. The available sections can
be seen in the Advanced Search dialog; the names of the predefined sections are
available as constants in the DocumentSection class.
Our implementation will put the book name
in the Subject/Heading section, the book authors in the Source/From section
(which is also used for e-mail sender names, for example), and the ISBN will be
treated as regular body text.
The indexing requests should be sent every time when the searchable content
of the resource is changed. Our plugin has only one place where books can be
changed, which is the Save handler of the book edit pane. That's where we will
put the indexing request, which is performed by a call to Core.TextIndexManager.QueryIndexing.
Editing and Deleting Books
Now we can create and view books, but there are two more operations that are critical for the plugin to be of any use: editing and deleting books.
Let's start with editing. We already have the main piece required to support
editing: the edit pane. What we still need is the action to invoke the editing
and the code in the EditResource
method to fill the edit pane with the values of the properties from the
edited book.
The edit book action is even simpler than the new book action. The only
difference is that the edit book action is not always enabled - we need to know
which book needs to be edited. Fortunately, there is a standard base class ActionOnSingleResource which covers one of our enabled requirements: one and
only one resource must be selected. The other requirement - specifying that the
selected resource must be a book - will be covered by specifying the resource
type in the action registration calls. We will provide the user with two ways to
invoke the action: double-clicking the book or right-clicking it and selecting
"Edit" from the context menu.
The delete book action is also fairly simple: since the book does not have
any complex connections, we can simply delete the book resource, and the system
will handle deleting the links from it to other resources. One thing to note is
that the action supports multi-selection, so we need to loop through all selected
resources instead of acting on the first one. Another is that, in order to
improve UI responsiveness, the resources are deleted asynchronously.
ResourceProxy is used to queue the deletions to the resource thread.
Registration of the delete book action is more interesting, though. The system already has several UI elements for the "Delete" action (a menu item in the Edit menu, a button on the toolbar, the Del key shortcut), and we want to apply all of them to our resource, instead of, for example, adding a new menu item "Delete Book" next to the "Delete" item. This is handled through composite actions. A composite action is an action which contains a single UI presentation but multiple implementations provided by different plugins. The specific implementation to be executed is selected based on the action context (active tab, type of the selected resources and so on). The plugin registers an action component, which references the composite ID ("Delete" in our case) and fits into all the composite actions (menu, keyboard and so on) which share the same composite ID.
Conclusion
We have now seen all the basic steps required to implement a plugin which defines a new resource type. The features which have been implemented are: creating, viewing, editing, searching and deleting books.
There are many more things we could have covered, including sidebar pane registration, resource icons, workspace integration and more. We will leave all that for the second part of the tutorial. And for now - feel free to explore the API, and don't hesitate to ask questions by e-mail or in the Omea newsgroups!