This post extends the approach shown in this previous post to implement a realistic editing and storage mechanism for application settings. It uses the .NET PropertyGrid control to display a custom class, allowing editing of a number of properties. This class is also serializable, which means we can use the .NET Framework to save it out to an XML file on disk. Some readers may have their own approaches to saving custom application settings, whether in the Registry or elsewhere: this post is primarily about displaying properties rather than providing a definitive "how to" for storing custom application settings. I chose a path of relatively low resistance, which will hopefuly prove interesting to some of the people reading the post.
MSDN contains a useful page on implementing the PropertyGrid in your project, but there are lots of other helpful pages you'll find on The Code Project and other sites.
Here's a project containing the code from this post, in case you'd prefer not to create it yourself.
As in the first part of the series, we need to add a User Control to our project. Within this control we'll add a single PropertyGrid, drawn to the full extents of the control (I found that drawing it to fill the container and then setting "Anchor" to "Top, Bottom, Left, Right" worked better than setting "Dock" to "Fill"). Thinking about it, it would probably work just to create a PropertyGrid in code and pass that into the constructor of the TabbedDialogExtension object, but doing it this way allows us to make use of the designer to play around with the control's properties at design-time, rather than making the settings dynamically at runtime.
Here's an idea of what the design should look like of our user control containing the property grid (nothing very impressive or exciting, at this stage - I'm basically just including it for completeness :-):
I customized the layout of the PropertyGrid somewhat - modifying the font and the background colour of the categories - but you will see that from the below snapshots or from the sample project.
Next we need to add some code. Here's the code behind this control, where we use the "value changed" event to signal that our tab's data is "dirty" and may require saving:
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
namespace OptionsDlg
{
public partial class OptionsTabControl : UserControl
{
public OptionsTabControl()
{
InitializeComponent();
}
private void propertyGrid_PropertyValueChanged(
object sender,
System.Windows.Forms.PropertyValueChangedEventArgs e
)
{
TabbedDialogExtension.SetDirty(this, true);
}
}
}
Here's the code for the rest of our application's implementation (stored in a separate .cs file - I called mine Application.cs, although you might prefer to split it into AppSettings.cs and Initialization.cs):
using System;
using System.Web.UI;
using System.IO;
using System.Xml.Serialization;
using System.ComponentModel;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
[assembly:
ExtensionApplication(
typeof(OneNeedsOptions.Initialization)
)
]
namespace OneNeedsOptions
{
public enum Fruit
{
Orange,
Banana,
Strawberry,
Apple
}
[Serializable(),
DefaultProperty("Name")
]
public class AppSettings
{
// Our internal properties
private string _name = "Kean Walmsley";
private string _url =
"http://blogs.autodesk.com/through-the-interface";
private DateTime _birthday = new DateTime(1912, 7, 14);
private Fruit _fruit = Fruit.Strawberry;
// Their external exposure and categorization/description
[Description("The person's name"),
Category("Identity")
]
public string Name
{
set { _name = value; }
get { return _name; }
}
[Description("The blog written by this person"),
Category("Stuff I do"),
UrlProperty()
]
public string Blog
{
set { _url = value; }
get { return _url; }
}
[Description("The day this person was born"),
Category("Identity")
]
public DateTime Birthday
{
set { _birthday = value; }
get { return _birthday; }
}
[Description("The person's age"),
Category("Identity"),
ReadOnly(true)
]
public int Age
{
get
{
return
(int)((DateTime.Now - _birthday).Days / 365.25);
}
}
[Description("The person's favourite fruit"),
Category("Stuff I like")
]
public Fruit FavouriteFruit
{
set { _fruit = value; }
get { return _fruit; }
}
const string filename = "AppSettings.xml";
// Our methods for loading and saving the settings
// Load needs to be static, as we don't yet have
// an instance
public static AppSettings Load()
{
AppSettings ret = null;
XmlSerializer xs = null;
StreamReader sr = null;
try
{
xs = new XmlSerializer(typeof(AppSettings));
sr = new StreamReader(filename);
}
catch
{
// File not found: create default settings
return new AppSettings();
}
if (sr != null)
{
ret = (AppSettings)xs.Deserialize(sr);
sr.Close();
}
return ret;
}
// Save will be called on a specific instance
public void Save()
{
try
{
XmlSerializer xs =
new XmlSerializer(typeof(AppSettings));
StreamWriter sw =
new StreamWriter(filename, false);
xs.Serialize(sw, this);
sw.Close();
}
catch (System.Exception ex)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"\nUnable to save the application settings: {0}",
ex
);
}
}
}
class Initialization : IExtensionApplication
{
static AppSettings _settings = null;
public void Initialize()
{
Application.DisplayingOptionDialog +=
new TabbedDialogEventHandler(
Application_DisplayingOptionDialog
);
}
public void Terminate()
{
Application.DisplayingOptionDialog -=
new TabbedDialogEventHandler(
Application_DisplayingOptionDialog
);
}
private static void OnOK()
{
_settings.Save();
}
private static void OnCancel()
{
_settings = AppSettings.Load();
}
private static void OnHelp()
{
// Not currently doing anything here
}
private static void OnApply()
{
_settings.Save();
}
private static void Application_DisplayingOptionDialog(
object sender,
TabbedDialogEventArgs e
)
{
if (_settings == null)
_settings = AppSettings.Load();
if (_settings != null)
{
OptionsDlg.OptionsTabControl otc =
new OptionsDlg.OptionsTabControl();
otc.propertyGrid.SelectedObject = _settings;
otc.propertyGrid.Update();
TabbedDialogExtension tde =
new TabbedDialogExtension(
otc,
new TabbedDialogAction(OnOK),
new TabbedDialogAction(OnCancel),
new TabbedDialogAction(OnHelp),
new TabbedDialogAction(OnApply)
);
e.AddTab("My Application Settings", tde);
}
}
}
}
The interesting stuff is in the AppSettings class: it defines a number of properties (for which I've set default values as they're declared - you could also put them in a constructor, should you so wish), which are then exposed externally. It's these public properties that are interesting, as we've used attributes to indicate how the properties should be categorized, described and whether they're editable. The rest of the class contains the protocol to load and save the settings: we use the .NET Framework to do the heavy lifting of saving the contents to a file (which we've simply called AppSettings.xml, without specifying the location, which means it will be stored wherever your module is located), and loading them back in again.
Here's what the XML content looks like for the default settings, in case you're interested, although you should never really need to worry about it, unless you're interested in allowing more direct modification of the file contents:
<?xml version="1.0" encoding="utf-8" ?>
<AppSettings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Kean Walmsley</Name>
<Blog>http://blogs.autodesk.com/through-the-interface</Blog>
<Birthday>1912-07-14T00:00:00</Birthday>
<FavouriteFruit>Strawberry</FavouriteFruit>
</AppSettings>
A couple of more comments on the code...
We haven't bothered implementing the Help callback, but I've left it in their for your convenience (we could also have passed a null value into the construction of the TabbedDialogExtension object). From the callbacks for OK and Apply we call through to the AppSettings class to save the data; from Cancel we reload the last saved state, effectively cancelling any unsaved changes.
The AppSettings class will need to be accessible from elsewhere in your code (that's the point, really - settings aren't much use unless they're accessed), but I haven't actually shown this. It should simply be a matter of setting the _settings object to be public (or internal), or of exposing the data you care about via properties on the Initialization class.
Here's what tab looks like, once we've built the application, loaded it and launched the OPTIONS command inside AutoCAD:
As you edit the properties you'll see the controls available suit the property in question: there's a date picker for "Birthday" (which isn't actually my date of birth, by the way: it seems safer not to publish your birthday on the web, these days) and a combo-box for "FavouriteFruit". I wish there were better display of URLs in the grid, but that appears to be a standard complaint, and beyond the scope of this post. You will notice an "Age" property which has been made read-only as it's calculated from the date field. You'll also notice that the "Name" property is selected by default, because we indicated it as such using the DefaultProperty() attribute of the class.