Beruflich Dokumente
Kultur Dokumente
NET
Mike Tuersley Ohio Gratings, Inc.
CP118-1
Almost every program has default or user settings such as folder locations, printing devices, color, or font settings. The question is how and where to let the user change them when needed. The solution is simple: place them with settings in AutoCAD by adding a tab for your program to the Options dialog box. Now the information is in a central location that is easy to modify and familiar to the CAD user. This session will show the .NET programmer how to add a tab to the Options dialog and where to store the information, including the pros and cons for each possibility. Storage options covered are the registry, AutoCAD or application configuration file, as well as a custom XML configuration file. Two of the subtopics shown will be how to serialize and deserialize XML data.
Introduction
One of the key design goals of a developer, or hobbyist programmer, is to have their application addin appear as if it is part of the original program; to seamlessly integrate it into the host environment. To accomplish this lofty goal, one must use the same user interface (UI) constructs as the host application wherever possible. Taking this into consideration, what is the best method for allowing the AutoCAD user to adjust whatever settings your program requires? There are many different approaches. Some have added special commands accessible from the command line, included commands within their unique interface, required users to modify external files such as INI files. The easiest and most logical solution is to allow access somewhere that is already intuitive to the user and easily accessible to them the AutoCAD Options dialog. In addition to looking at how to add a tab to the Options dialog, I will also look at how to store and recall the data that will be placed there using methods such as XML Serialization and Isolated Storage.
2. Add references to the AutoCAD managed libraries: acmgd.dll and acdbmgd.dll 3. Go to the code view of the user control [UserControl1 unless its been renamed] 4. Add the Imports statements for Windows Forms, AutoCADs Application and ApplicationServices. I like to add them like so to eliminate typing later: 2
To explain the code above: Lines 1-12: stubs for the three events available for the Options Tab Line 13: starts the hook that will add the UserControl as a tab in the Options tab collection whenever the Options command is called within AutoCAD Line 16: instantiates a new instance of the user control Line 17-24: adds the user control to AutoCADs Option dialog box as another tab including the event handlers for the Option dialogs OK, CANCEL and HELP buttons 6. Save the project 7. Start the debug command 8. Whenever AutoCAD opens, type in NETLOAD and browse to the location of the dll generated by this project 9. After netloading, type tabdemo into the command line to activate the dll
10. Go into AutoCADs Options by either menu selection [Tools>Options] or typing in OP at the command line. If all worked well, a new tab called .AU2009 should be included in the Options dialog:
That is as hard as it gets to add a tab to the Options dialog box. Now code will need added to the OnOk, OnCancel and OnHelp subroutines to handle those specific functions and we will do it further on within this document. In addition to adding the usercontrol to the Options dialog, the same control could be added to the Drafting Settings and Customization dialogs by adding these lines to the DoIt subroutine to accommodate them:
AddHandler cadApp.DisplayingDraftingSettingsDialog, _ AddressOf Application_TabbedDialog AddHandler cadApp.DisplayingCustomizeDialog, _ AddressOf Application_TabbedDialog
Storage considerations
Now that we have a place for the CAD user to adjust any settings our program requires, we need to consider how data should be stored. Typically, programmers seem to choose one of two options: 1. Registry the programmer stores all data within the registry. Some think this makes it more secure and easier to hide than using a file on the hard drive. This is a bad idea for three reasons a. With the free registry tracking software available, the idea that this is hidden or safer than a physical file isnt valid. Some of the registry cleaning software can even remove the programs data if the programmer does not store it correctly. b. The end user may not have the rights necessary to change the data. c. Even under the most stringent precautions, incorrectly writing to the registry can crash a computer.
2. Physical file while better than the registry, there are more hurdles here than appear at first glance. Some of the questions involved are: a. Location where will the file be located? Some place hard coded but what if the file moves? Specified at install but how to know where? b. Format what is the best format to store the data in? Simply write each value to a line and track which line is which when reading back? Using Comma-Separated Values (CSV) then worrying about tracking locations and hoping the user doesnt add data that includes the delimiter? Or having to re-dimension arrays/collections if the amount of fields changes with either of these? Use an INI file but having to write a custom parser? These are just some of the things to consider when writing a custom application that will be delivered to a client whether the client is someone who paid for this, or someone within your own organization. Unfortunately there is no standard for where to store data; therefore, I offer a compilation of what has worked and what hasnt over the past twenty years of creating custom applications for Autodesk products. Programming information that needs to be stored and/or retrieved generally falls into three categories: Admin the data that the program requires in order to function that will rarely be changed and, usually, only by an administrator-level person. Standard the data that is specific to a group of users; office or project standards; the data is changed less frequently than the User data and typically by a central person User the data that is specific to the user using the program at that particular time; each user has their own unique settings that they change to suit their tastes
To accommodate these three types of data, well use a combination of registry and physical files with a few twists added to make it easy. Figure 3 depicts the overall structure of the end solution. We will be using the registry for Admin data and local file(s) for User data. Standard data would be placed in a networked location if this application had such a function. In some cases such as dealing with remote users, the Standard type data could be stored local to the user but in a different location than the users personal data (since the sample application detailed within this document is selfcontained, there will be no networked requirement). So now that the general location of the data has been determined, what is the format that will be used? To solve this, lets consider the end result. Once the data has been read into the application, it would be nice to have the data easily accessible throughout the program. This would dictate creating a custom object to hold the data since it is easier to use than passing around arrays, collections or hashtables and trying to remember which data is in which location. In order to read/write the custom object to a physical file, we will need to first create a parser to read in the values and distribute them to the correct properties of the custom object. This parser will also be required to write out the data to the physical file. Finally, the parser should be generic enough to handle additions/subtractions of properties that could occur during the course of the project as well as future modifications to it. By now, you must be thinking there is an awful lot of work involved here. It would be nice if there was a way to have this functionality without writing the parser, right? Well there is a way to handle the parser in 4 to 5 lines of code use XML. XML, the eXtensible Markup Language, was created for just this scenario. First, the language creates meaningful, self-describing files so its easier to read than using something like CSV. Second, XML includes its own internal functionality for saving the data to a file called Serialization. And, third, it can be done without creating XML Schema Definition (XSD) or Document Structure Definition (DSD) files. If you have never used XML files before, or were frustrated when attempting to use them, you will not believe how simple this is!
This attribute tells the system that the structure can be serialized. It is important to note that only the basic data types defined within the .NET Framework can be serialized; if using custom objects as properties, more work is required than just adding the attribute. Now, as promised, 4 lines of code to save out our structure object to disk:
1 2 3 4
Dim writer As New XMLSerializer(GetType($MyStructureName$)) Dim file As New StreamWriter($FileNameToSaveAs$) writer.Serialize(file, $MyStructureObject$) file.Close()
Line 1 declares and instantiates an instance of an XMLSerializer object using the object type of our structure. Line 2 declares and instantiates an instance of a standard StreamWriter object and we pass the name of the output file Line 3 does the entire writing process Line 4 cleans up the StreamWriter object The end result is a file that looks similar to this:
<UserDefaults> <PrinterName>Whatever the value is</PrinterName> <PrinterPaper>Whatever the value is</PrinterPaper> </UserDefaults>
As shown, the resulting file is a lot easier to read than a CSV file. To read the file back into our object, this code is required:
1 1 1 4 Dim reader As New XmlSerializer(GetType($MyStructureName$)) Dim file As New StreamReader($FileName$) Dim fileData = CType(reader.Deserialize(file), $MyStructureObject$) file.Close()
Line 1 declares and instantiates an instance of an XMLSerializer object using the object type of our structure. Line 2 declares and instantiates an instance of a standard StreamReader object and we pass the name of the saved file Line 3 does the entire reading and populating our structure process Line 4 cleans up the StreamReader object Notice that there is no need to read each line and populate each property of our new structure object which would be required when importing data from a CSV or similar file.
From here, you use normal System.IO file handling methods such as XML serialization that was discussed in the previous section. As seen in the code above, there is no need to specify the path as the isolated storage location is handled internally by Windows.
Arrange and set them up as seen above in Figure 4. The data is separated into the three groupboxes to simulate the three types of data. When this is complete, lets focus on the specific functions needed to save the data into the required locations. As demonstrated in the previous section, we will implement a serialization function to our structure:
1 2 3 4 5 6 7 8 9 10 11 12
Public Sub SerializeMe() Try Dim isolatedStore As IsolatedStorageFile = _ IsolatedStorageFile.GetStore(IsolatedStorageScope.User Or _ IsolatedStorageScope.Assembly, Nothing, Nothing) Dim isoStream As New IsolatedStorageFileStream(Options.dat, _ FileMode.Append, FileAccess.Write, isolatedStore) Dim writer As New XmlSerializer(GetType(UserDefaults)) Using file As New StreamWriter(isoStream) writer.Serialize(file, Me) End Using Catch ex As Exception nothing for now End Try End Sub
This code does two things. First it finds the users isolated storage location and then serializes the structure out to a file. We are using Isolated Storage so we do not need to worry about where the file is located or if it will ever move there is no need to worry or code for any of that any more if you use the Isolated Storage mechanism. Lines 5-8 do the actual serialization and include the USING construct so we do not need to worry about closing the objects or disposing of them.
Applying
Lastly, we need to look at how to turn on the APPLY button functionality of the Options dialog if you havent noticed, it has been disabled so far. This is really simple every place where a value is changed such as a Combobox SelectedValue event add the following line of code:
TabbedDialogExtension.SetDirty(Me, True)
This tells AutoCADs internal Options dialog handler that a value has changed that needs saved and, thus, the Apply button will be enabled. When the Apply button is selected, the button will automatically become disabled once the OnApply code has run. To manually disable the Apply button, use the same line of code and change True to False.
Closing
This completes the topic on Adding Settings to the AutoCAD Options Dialog with VB.NET. Hopefully, the discussion of how and where to store your future programming data as well as some of the specific tips and tricks on XML and VB.NET in general, will prompt you to continue exploring in these areas. The complete source code for this session will be available on the Autodesk University website and at www.mtuersley.com/downloads. If you have any questions pertaining to this topic, please email me at mike.tuersley@mtuersley.com. Make sure to include a reference to this session within the email subject so it will not get lost amongst the other email I receive. Thank you for your time and I hope you enjoy your time here at AU2009! 10
Public Class OptionsTab2 Public Const ConfigFileName As String = "OptionsTab.dat" Private Shared _mySettings As New UserDefaults Private _DefaultFolder As String = String.Empty #Region "Event Handling" Public Sub OnApply() 'save settings SaveSettings() End Sub Public Sub OnOk() 'save settings SaveSettings() End Sub Public Sub OnCancel() 'do nothing End Sub Public Sub OnHelp() 'do nothing End Sub #End Region <Autodesk.AutoCAD.Runtime.CommandMethod("tabdemo2")> _ Public Shared Sub DoIt() AddHandler cadApp.DisplayingOptionDialog, AddressOf Application_TabbedDialog End Sub
11
Private Shared Sub Application_TabbedDialog(ByVal sender As Object, _ ByVal e As cadAppSrvs.TabbedDialogEventArgs) Dim ctrl2 As OptionsTab2 = New OptionsTab2() e.AddTab(".NET Demo2", _ New cadAppSrvs.TabbedDialogExtension( _ ctrl2, _ New cadAppSrvs.TabbedDialogAction(AddressOf ctrl2.OnOk), _ New cadAppSrvs.TabbedDialogAction(AddressOf ctrl2.OnApply), _ New cadAppSrvs.TabbedDialogAction(AddressOf ctrl2.OnCancel), _ New cadAppSrvs.TabbedDialogAction(AddressOf ctrl2.OnHelp) _ ) _ ) End Sub #Region "Control Code" Private Sub btnBlockFolder_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnBlockFolder.Click 'set dialog description dlgFolderBrowse.Description = "Select the folder..." dlgFolderBrowse.SelectedPath = Me.txtBlockFolder.Text dlgFolderBrowse.ShowNewFolderButton = True 'display it and test for return value If dlgFolderBrowse.ShowDialog(Me).Equals(DialogResult.OK) Then 'set the textbox txtBlockFolder.Text = dlgFolderBrowse.SelectedPath End If 'set dirty flag cadAppSrvs.TabbedDialogExtension.SetDirty(Me, True) End Sub Private Sub cboPlotter_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cboPlotter.SelectedIndexChanged 'printing device changed so clear then 'grab media specific to selected device cboPaper.Items.Clear() cboPaper.Text = String.Empty GetDeviceMedia(cboPlotter.Text, cboPaper) 'store the value _mySettings.PrinterName = cboPlotter.Text 'set dirty flag cadAppSrvs.TabbedDialogExtension.SetDirty(Me, True) End Sub Private Sub cboPaper_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles cboPaper.SelectedIndexChanged 'store the value _mySettings.PrinterName = cboPaper.Text 'set dirty flag cadAppSrvs.TabbedDialogExtension.SetDirty(Me, True) End Sub
12
Private Sub OptionsTab2_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load _mySettings = New UserDefaults _mySettings.DeserializeMe() 'populate printing devices LoadPrintingDevices2Combobox(Me.cboPlotter) _DefaultFolder = My.Settings.DefaultFolder txtBlockFolder.Text = _DefaultFolder cboPlotter.SelectedText = _mySettings.PrinterName If _mySettings.PrinterName IsNot Nothing Then If Not _mySettings.PrinterName.Equals(String.Empty) Then GetDeviceMedia(_mySettings.PrinterName, Me.cboPaper) cboPaper.SelectedText = _mySettings.PrinterPaper End If End If ' PictureBox1.Image = GetEmbeddedIcon("OptionsTab.au09-badge1.jpg") 'set dirty flag cadAppSrvs.TabbedDialogExtension.SetDirty(Me, False) End Sub #End Region #Region "Functions" ''' <summary> ''' Gets the media names from the AutoCAD printing device ''' </summary> ''' <param name="DevName">String - Name of printing device</param> ''' <param name="cb">ComboBox - combo to populate</param> ''' <remarks></remarks> Public Sub GetDeviceMedia(ByVal DevName As String, ByRef cb As ComboBox) ' Retrieve printing media names Dim medname As String = String.Empty Dim psv As PlotSettingsValidator = PlotSettingsValidator.Current Dim ps As New PlotSettings(True) Using ps ' refresh the list psv.SetPlotConfigurationName(ps, DevName, Nothing) psv.RefreshLists(ps) Dim medlist As StringCollection = psv.GetCanonicalMediaNameList(ps) For i As Integer = 0 To medlist.Count - 1 'ed.WriteMessage(vbLf & "{0} {1}", i + 1, medlist(i)) cb.Items.Add(medlist(i)) Next cb.Sorted = True End Using End Sub Public Shared Function GetEmbeddedIcon(ByVal strName As String) As Bitmap 'pulls embedded resource Return New System.Drawing.Bitmap _ (GetExecutingAssembly.GetManifestResourceStream(strName)) End Function
13
''' <summary> ''' Populates supplied combobox with AutoCAD printing device names ''' </summary> ''' <param name="cb">ComboBox - combo to populate</param> ''' <remarks></remarks> Public Shared Sub LoadPrintingDevices2Combobox(ByRef cb As ComboBox) ' Load all AutoCAD printer names to a combobox Dim devname As String = String.Empty Dim psv As PlotSettingsValidator = PlotSettingsValidator.Current Dim devlist As StringCollection = psv.GetPlotDeviceList() For i As Integer = 0 To devlist.Count - 1 'ed.WriteMessage(vbLf & "{0} {1}", i + 1, devlist(i)) cb.Items.Add(devlist(i)) Next End Sub Private Sub SaveSettings() 'test for value If Not txtBlockFolder.Text.Equals(String.Empty) Then 'set value and save _DefaultFolder = txtBlockFolder.Text My.Settings.DefaultFolder = _DefaultFolder End If _mySettings.SerializeMe() 'reset dirty flag cadAppSrvs.TabbedDialogExtension.SetDirty(Me, False) End Sub #End Region End Class Imports System.IO Imports System.IO.IsolatedStorage Imports System.Xml.Serialization <Serializable()> _ Public Structure UserDefaults Public Const ConfigFileName As String = "OptionsTab.dat" Private printerNameField As String Private printerPaperField As String Public Property PrinterName() As String Get Return Me.printerNameField End Get Set(ByVal value As String) Me.printerNameField = value End Set End Property
14
Public Property PrinterPaper() As String Get Return Me.printerPaperField End Get Set(ByVal value As String) Me.printerPaperField = value End Set End Property #Region "Methods" Public Sub DeserializeMe() Try Dim isolatedStore As IsolatedStorageFile = _ IsolatedStorageFile.GetStore(IsolatedStorageScope.User Or _ IsolatedStorageScope.Assembly, Nothing, Nothing) Dim isolatedStream As New IsolatedStorageFileStream(ConfigFileName, _ FileMode.Open, isolatedStore) Dim reader = New XmlSerializer(GetType(UserDefaults)) Using file As New StreamReader(isolatedStream) Dim fileData = CType(reader.Deserialize(file), UserDefaults) PrinterName = fileData.PrinterName PrinterPaper = fileData.PrinterPaper End Using Catch ex As Exception End Try End Sub Public Sub SerializeMe() Try Dim isolatedStore As IsolatedStorageFile = _ IsolatedStorageFile.GetStore(IsolatedStorageScope.User Or _ IsolatedStorageScope.Assembly, Nothing, Nothing) Dim isoStream As New IsolatedStorageFileStream(ConfigFileName, _ FileMode.Append, FileAccess.Write, isolatedStore) Dim writer As New XmlSerializer(GetType(UserDefaults)) Using file As New StreamWriter(isoStream) writer.Serialize(file, Me) End Using Catch ex As Exception End Try End Sub #End Region End Structure
15