How can I: implement a Tools > Options dialog box?

Topics: CAB & Smart Client Software Factory
Mar 6, 2007 at 9:46 PM
Hi,

I'm looking at an empty project and trying to figure out how I could accomplish this:

An options/properties dialog box. Some settings will be generic, some will be module specific and other settings will be service specific.

I need to think about these:
- Saving the values (user settings or app.config ?)
- Modal window that mimics the Options form in VS2005
- Menu Toolbar: Tools > Options
- Shortcut Keys
- Automatic popup in certain circumstances

I believe I'll need a WindowWorkspace and a PropertiesView UserControl. This UserControl would host a TreeView and a DeckWorkspace.

In a Shell + ShellLayoutView scenario, do I put that UserControl in Infrastructure.Layout module or a separate module?

Too many questions I bet.

Pat
Mar 7, 2007 at 12:32 AM

In a Shell + ShellLayoutView scenario, do I put that UserControl in Infrastructure.Layout module or a separate module?


I'd be inclined to put it in a separate module. The rest of it sounds good; sounds like the basic UI components.

As for saving: I'd put the values in their own file and not couple them to anything else (like the app.config). I think it would be easier to isolate, test and debug. The modal window aspect can be achieved with the WindowWorkspace.
Mar 9, 2007 at 5:41 PM
Edited Mar 9, 2007 at 5:43 PM
This is something I've been working on myself. My approach is to put a form into my infrastructure shell app that has a tab workspace that is registererd with a specific name in the Infrastructure Interface Constants.

This tab workspace is generic in that it knows nothing about the options controls that will reside in it. It just opens and displays the tab workspace. It also has save/cancel buttons and raises the global OnSaveOptions EventTopic that the modules can respond to. This tab workspace form subscribes to the OnRegisterOptionView EventTopic that can be published by any module that needs to have its options displayed in the tab workspace. The option box opens upon reciept of a global OnShowOptions Event topic (typically raised by a CommandHandler) and displays tabs for each option view that has been registered. This way, modules can register their options views in an decoupled way from the infratstructure yet provide module specific options views.

Each module's OptionsView/Presenter is responsible for saving its own settings when the OnSaveOptions EventTopic fires. Usually this infomration is saved in the Modules.WorkItem.State collection.
Mar 12, 2007 at 2:50 AM
Thanks for sharing, I think I will implement my options just as you have outlined here.

Do you pass the SmartPart in the event args of OnRegisterOptionView?
Mar 12, 2007 at 4:49 AM
I'm just about done with my first implementation pass on mcstar's design.

Usually I can work with the CAB OK, I need to check the ref. implementations to refresh my memory occasionally, but I do alright. Sometimes however I have a REAL hard time. I just had one of those times :(

A few aspects of my implementation gave me a real headache, it's my hope that I can share them here and other, more experienced users can comment on how I'm approaching things wrong and possibly offer a better solution. Hopefully other will learn from this as well.

I'll start with the view registration process for my various modules:

public override void Run()
{
    AddServices();
    ExtendMenu();
    ExtendToolStrip();
    AddViews();
 
    //  Register the options View
    ProductModuleOptionsView tmp = new ProductModuleOptionsView();
    WorkItem.EventTopics[Constants.EventTopicNames.RegisterOptionsView].Fire(
        this,
        new EventArgs<Type>(tmp.GetType()), 
        WorkItem, 
        Microsoft.Practices.CompositeUI.EventBroker.PublicationScope.Global);
}
 
[EventSubscription(Constants.EventTopicNames.RegisterOptionsView)]
public void OnRegisterOptionsView(object sender, EventArgs<Type> args)
{
    if(args.Data == null)
    {
        throw new ApplicationException("You need to supply a valid smart part to register");
    }
 
    object view = WorkItem.SmartParts.AddNew(args.Data);
    WorkItem.Workspaces[Constants.WorkspaceNames.OptionsWorkspace].Show(view);
}

This is ugly code. I haven't worked with generics much so I had a hard time figuring out how to get my module's view Type to the registration handler. The code above works, but I'm not happy with it. The part that bothers me is in the Run() method, I had to instantiate an instance of my view to get it's Type, the fact that I had to do this makes me think I have an error in my overall approach. Is there a better way to accomplish passing the Type to the registration handler?

The second aspect I had trouble with was showing the OptionsView (tabbed workspace), here is my code for that (from Infrastructure.Module.ModuleController):
private void AddViews()
{
    IOptionsView view = WorkItem.SmartParts.AddNew<OptionsView>("AAA");
}
 
[CommandHandler(Constants.CommandNames.ShowOptionsView)]
public void OnShowOptionsView(object sender, EventArgs e)
{
    WorkItem.Workspaces[Constants.WorkspaceNames.ModalWindows].Show(WorkItem.SmartParts["AAA"]);
}

Note that I had to give my OptionsView and ID ("AAA") so that I could locate it later in the CommandHandler. This troubles me as I have never seen anything in the reference implementations that show giving Views an ID, makes me think I might be doing this wrong. I tried to search for the View by type but couldn't get that to work. WorkItem.SmartParts.FindByType<Type>() returns System.Collections.Generic.ICollection (I think) and it wouldn't allow indexed access to the collection.

So like I said, I've got it working, but I feel like I've done some things wrong or that I could have done them better.

I welcome any comments or suggestions.

Thanks for reading,
Steve
Mar 12, 2007 at 7:02 AM
Funny how a good design can make everything OK :)

After realizing that I didn't have access to my module's services in the ProductModuleOptionsView's presenter I changed my code to add the view to the module work item and pass the instance to the register command. This solved everything and makes more sense now that I think about.

Lesson learned.

Sorry for all the posts, hopefully someone will get something useful out of all this.
Apr 3, 2007 at 3:13 AM
Hi Steve,

Could you please post the final (sample) code you're using? I'm dealing with exactly the same issue, and can't figure out how to implement this.

I'd really appreciate it!
Apr 5, 2007 at 2:40 AM
Hi SimpleUser,

Sorry it's taken so long to get back to you.

I based my design on mcstar's design which made complete sense and sounded simple enough. Here are the details:

I have a View in my Infrastructure.Module project that has a single TabWorkspace and a Save and Cancel button.

In my Infrastructure.Module ModuleController I have these event subscriptions:
[EventSubscription(Constants.EventTopicNames.RegisterOptionsView)]
public void OnRegisterOptionsView(object sender, EventArgs<object> e)
{
    if(e.Data == null)
    {
        throw new ApplicationException("You need to supply a valid smart part to register");
    }
 
    //object view = WorkItem.SmartParts.AddNew(e.Data);
    WorkItem.Workspaces[Constants.WorkspaceNames.OptionsWorkspace].Show(e.Data);
}
 
[CommandHandler(Constants.CommandNames.ShowOptionsView)]
public void OnShowOptionsView(object sender, EventArgs e)
{
    WorkItem.Workspaces[Constants.WorkspaceNames.ModalWindows].Show(WorkItem.SmartParts[_optionsDisplayViewName]);
}

And I add the options layout and system options views like this:
private void AddViews()
{
    IOptionsDisplayView view = WorkItem.SmartParts.AddNew<OptionsDisplayView>(_optionsDisplayViewName);
 
    //  Add the system options view
    ISystemOptionsView optionsView = WorkItem.SmartParts.AddNew<SystemOptionsView>();
    WorkItem.Workspaces[Constants.WorkspaceNames.OptionsWorkspace].Show(optionsView);
}

In my Infrastructure.Module OptionsLayoutView Presenter I have this:
[EventPublication(Constants.EventTopicNames.OptionsViewSaved, PublicationScope.Global)]
public event EventHandler<EventArgs> OptionsViewSaved;
 
[EventPublication(Constants.EventTopicNames.OptionsViewCancelled, PublicationScope.Global)]
public event EventHandler<EventArgs> OptionsViewCancelled;
 
public void SaveOptions()
{
    if(OptionsViewSaved != null)
    {
        OptionsViewSaved(this, EventArgs.Empty);
    }
}
 
public void CancelOptions()
{
    if(OptionsViewCancelled != null)
    {
        OptionsViewCancelled(this, EventArgs.Empty);
    }
}

I've created a global options service for options that are shared by the entire application. I have also added a "SystemOptionsView" in my Infrastructure.Module project. This view represents the options for the entire application.

In the module controller of each module I do the following:
- Add a <modulename>OptionsService Service to the workitem.
- Add the <modulename>OptionsView to the workitem
- register the view by firing an event that the Infrastructure.Module subscribes to.

Here is some sample code from a business module:
private void AddServices()
{
    WorkItem.RootWorkItem.Services.AddNew<FirmwareModuleOptionsService, IFirmwareModuleOptionsService>();
    WorkItem.RootWorkItem.Services.AddNew<FirmwareService, IFirmwareService>();
}
 
private void AddViews()
{
    //  Add the options views
    IFirmwareModuleOptionsView optionsView =
        WorkItem.SmartParts.AddNew<FirmwareModuleOptionsView>();
 
    //  Fire the event to register it with the shell, later I will
    //  change this to use an Action so it respects Roles...
    WorkItem.EventTopics[Constants.EventTopicNames.RegisterOptionsView].Fire(
        this,
        new EventArgs<object>(optionsView),
        WorkItem,
        Microsoft.Practices.CompositeUI.EventBroker.PublicationScope.Global);
}

Here are the saving/cancelling subscriptions that each of my <modulename>OptionsViewPresenters have:
[EventSubscription(EventTopicNames.OptionsViewSaved)]
public void OnOptionsViewSaved(object sender, EventArgs e)
{
    //  save the options
    _optionsService.SaveOptions();
}
 
[EventSubscription(EventTopicNames.OptionsViewCancelled)]
public void OnOptionsViewCancelled(object sender, EventArgs e)
{
    //  If the model supports checking for dirty, cancel the changes
 
}


I hope that makes sense.
Here is a screen shot of what the result looks like (ugly UI, I know, I know...)
http://www.pmddirect.com/temp/options_view.png

-Steve
Apr 5, 2007 at 3:41 AM
Thank you Steve! This is exactly what I needed, You saved me a lot of time!

This only thing I don't agree with you is your "ugly UI" comment! It's simple, and does the job.

Thank you again.
Apr 17, 2007 at 5:24 PM
I've run into a design snag and wanted to see what you all think.

I'm beginning to add validation code to my Option Views, soon I won't be the ONLY one using the application so I need to make sure only quality data is allowed to be entered. I would like to use the .net ErrorProvider to display a message next to a field that has an invalid or missing value. So far so good.

The problem I've encountered is that my individual option Views don't actually have a button to save their settings, rather the View they are hosted in has a Save and Cancel button, these buttons fire an event that the module option Views subscribe to. It's at this point that I had intended to do a final validation pass on the data being saved.

I quickly changed my OptionsSaved event to pass an EventArg<bool> that the subscribers can modify in the event that a data error is found and the view should not be closed, this works fine. Whether or not it is a good design is another story.
public void SaveOptions()
{
    if(OptionsViewSaved != null)
    {
        EventArgs<bool> args = new EventArgs<bool>(false);
        OptionsViewSaved(this, args);
 
        //  If non of the option views cancelled the save, close the view
        if(args.Data == false)
        {
            OnCloseView();
        }
    }
}

The last snag I hit is that if my user is looking at tab #3 (say, "Customer Options") and when they save tab #1 ("Product options") complains about missing data, I need to activate the view that complained about missing or bad data;
a) I don't know how to make a TabWorkspace show a specific tab as the active tab
b) My simple EventArg<bool> will need to be changed to a full custom EventArg type that also contains a reference (or list of references) to the smart parts that are in error.

Right about now I'm just wondering if there are better solutions out there, maybe someone here has an idea for a slicker way to handle this scenario?

Jun 12, 2007 at 9:27 PM
Edited Jun 12, 2007 at 9:33 PM
sklett and mcstar have posted a very inspiring solution.
However, I think it is too heavy, since those views can not be disposed during the whole life of the application.

So how about this:
1)Add a simple form OptionForm in the module that we want to display the options.
2)Every time when need to display the option, just publish a global event, see DisplayOptions, the EventArgs should has a collection like List<Control> as its data.
3)Every module has settings, simple has a UserControl or Panel, well, maybe follows an interface, IUserSettings, with methods as SaveSetting, (I believe view is not needed, just a panel is OK). Since this Panel has access to the module’s settings, we can simply new a setting control, subscribe to the DisplayOption global event topic (UI Thread), then add this setting control to the collection.
4)Now the OptionForm has all the setting controls from different modules collected. It can do the regular GUI stuffs. (In the Save method, just call every IUserSettings.SaveSetting in turn.)
5)After we done with it, we can dispose each setting control now.

The key point I want to make is to dynamicly instantiate the setting controls on demand, then dispose them immedialtly.