Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp .NET Developer's Handbook (2002) [eng]

.pdf
Скачиваний:
36
Добавлен:
16.08.2013
Размер:
4.94 Mб
Скачать

this example is a way to determine when the user has changed object settings and a method for freeing notification handles. Listing C.1 shows both MMC function implementations.

Listing C.1: MMC Example Functions

public __gc class MMCFunctions

{

public:

//Allows the property sheet to notify the MMC snap-in component

//that the user has made a change to the object settings.

HRESULT DoMMCPropertyChangeNotify(long INotifyHandle, LPARAM param)

{

return MMCPropertyChangeNotify(INotifyHandle, param);

}

//Frees the handle to an MMCN_PROPERTY_CHANGE message. The system

//sends this message as the result of a MMCPropertyChangeNotify()

//call by the property sheet.

HRESULT DoMMCFreeNotifyHandle(long lNotifyHandle)

{

return MMCFreeNotifyHandle(lNotifyHandle);

}

};

As you can see, the functions provide a direct pass through to Windows. You could implement these functions from within C#, but it's better to place them within the DLL used to wrap the interfaces, structures, enumerations, and other MMC specific code.

Developing the MMC Snap-In

Now that we have a wrapper DLL that contains all of the resources required to build an MMC snap-in, it's time to create the snap-in itself. As previously mentioned, an MMC snap-in must implement certain COM interfaces in order to interact with the MMC container application and the user. You can also divide the MMC snap-in into several functional areas, such as the About dialog, help system, component, property pages, and document data. The following sections describe several of these functional areas. You can find the complete application source code in the \Appendix C\MMCSample folder on the CD.

Warning During the development of this example, I noted that the example often failed to compile properly within the IDE. The code would compile, but the snap-in would fail to initialize within the MMC. The command-line version of C#, CSC.EXE, provides a simpler interface without all of the usual command-line switches that the IDE includes by default. The example code on the CD includes the MAKEFILE text file that contains instructions for the NMAKE utility. You can use the make file by opening a command prompt in the example directory on your hard drive and typing NMAKE. The make file will automatically generate the DLL, register it, add it to the global assembly cache (GAC), and register it as an MMC snap-in. You normally need to perform these three steps by hand when using the IDE, so using NMAKE is actually easier than using the IDE in this case.

Obtaining a GUID

As with all COM objects, MMC snap-ins rely on Registry entries to provide information about component configuration. The use of Registry entries means that you need some way to uniquely identify your component, which means using a globally unique identifier (GUID). Because Visual Studio .NET doesn't provide this information automatically as the older, native code versions of the product did, you need to obtain the GUID manually. Fortunately, you can use a utility named GUIDGen to create the required GUIDs. You can access this utility using the Tools Create GUID command from within the Visual Studio .NET IDE or from within the \Program Files\Microsoft Visual Studio .NET\Common7\Tools folder. Figure C.6 shows how this tool looks.

Figure C.6: The GUIDGen utility enables you to create GUIDs for your components.

Notice that the figure shows Option 4, Registry Format, selected. Make sure you always use this format for components created in C#. Click Copy to place the GUID on the clipboard. You can then place it within the application using the following code:

[Guid("B6BBA1A8-6D1C-47f9-A9CC-FAC427315CAF")]

public class MySnapInData : IComponentData, IExtendPropertySheet

{

};

Of course, the class will contain other information. The important piece here is the placement of the [Guid] attribute. You need to provide GUIDs for both the component and the About dialog. The GUIDs also appear in the Registry entry and serve to identify the snap-in to MMC. Here are the Registry entries for the example MMC snap-in. (Note that the entry wraps in some cases and that the actual entry would appear on a single line, as shown in the source code.)

REGEDIT4

[HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\Snapins\{B6BBA1A8-6D1C-47f9- A9CC-

FAC427315CAF}]

"About" = "{BACD4F1D-8338-41ee-9D55-DDECE3D8BBCE}" "NameString" = "An Example of an MMC Snap-In" "Provider" = "DataCon Services"

"Version" = "1.0"

[HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\Snapins\{B6BBA1A8-6D1C-47f9- A9CC-

FAC427315CAF}\StandAlone]

There are a few notes about this part of the example. I began by adding a new Text File to the project and simply renamed it with a REG extension. The file must begin with REGEDIT4 as shown, and you need to add the main GUID key. Notice the use of curly brackets around the GUID entry (they don't appear in the [Guid] attribute entry). Only provide the About value if your snap-in has an About dialog. The NameString, Provider, and Version values are also optional. You must provide the StandAlone key (as shown) if you're following this example— snap-in extensions require different programming techniques from the ones shown for this example.

Developing an About Dialog

Of all the elements of the example snap-in, create the About dialog was the easiest from a coding perspective, so I'll discuss it first. The purpose of the ISnapinAbout interface is to create the data for an About dialog—the methods within the interface don't actually create the About dialog. You'll find the code for this interface in the MySnapInAbout class. MMC uses the data this interface provides in several places, not the least of which is the Add Standalone Snap-in dialog box.

In addition to text, the MySnapInAbout class provides four icons to MMC for use with the snap-in entry—don't confuse these icons with the ones you'll use for the nodes later on. The four icons include: a main icon used for elements such as the About dialog box, a large 32 × 32 icon used for display in the Results pane when the user selects the root node, a small 16 × 16 icon that shows the snap-in in its closed state, and a small 16 × 16 icon that shows the snap-in in its open state. In many cases, you can use the same icon for both open and closed states. Most snap-ins create the required icons in the constructor and destroy them in the destructor.

The About dialog is one of the few places where you actually need a destructor, because we're using Windows resources. MMC doesn't understand managed resources, so you need to convert the embedded bitmap into an icon handle that MMC will understand. Because the resources aren't managed by the Garbage Collector, you need to destroy them manually. Listing C.2 shows the code used to create and destroy the image resources required for this example.

Listing C.2: Working with MMC Means Creating and Deleting Unmanaged Resources

public MySnapInAbout()

{

// Retrieve an embedded bitmap from the current assembly. Assembly Asm = Assembly.GetExecutingAssembly();

Stream

Strm

=

Asm.GetManifestResourceStream("MMCSample.Main.bmp");

Bitmap

Temp

=

new Bitmap(Strm);

//Place a handle for an icon based on the bitmap in MainIcon. MainIcon = Temp.GetHicon();

//Create compatible bitmaps for the two bitmap images. BMap16 = new Bitmap(Temp, 16, 16).GetHbitmap();

BMap32 = new Bitmap(Temp, 32, 32).GetHbitmap();

}

~MySnapInAbout()

{

// Deallocate the memory used by the three images. if (MainIcon != IntPtr.Zero)

WindowsFunctions.DestroyIcon(MainIcon); if (BMap16 != IntPtr.Zero)

WindowsFunctions.DeleteObject(BMap16); if (BMap32 != IntPtr.Zero)

WindowsFunctions.DeleteObject(BMap32);

}

Notice that the example obtains the required bitmap from an embedded resource using reflection. The code creates a main icon handle using the GetHicon() method and two bitmap handles using the GetHbitmap() method. Each image resource type appears in a different place within MMC. Because the image types are different from each other, you also need to use the correct Windows API calls to destroy them. Icons require use of the DestroyIcon() function, while bitmaps rely on the DeleteObject() function.

Creating the Component

This example shows you how to create a simple single node MMC snap-in that displays GetComputerNameEx() outputs. As a result, we won't be doing anything with the MMC Scope Pane.

There are two main sections of code for the component portion of this example. The first processes event MMC requests. The second displays the requested data. In most circumstances, you could combine these two tasks into one method call. However, MMC uses a callback mechanism to display items within the Result View Pane. Each item is processed on a column by column basis. The callback function will need to detect the current column and provide only the string required for that column, rather than fill in all of the columns at one time. Listing C.3 shows how to create the MMCN_SHOW event handler in the MySnapInData.Notify() method.

Listing C.3: Handling MMCN_SHOW Events in an MMC Snap-In

public int Notify(IDataObject lpDataObject, uint aevent,

IntPtr arg, IntPtr param)

{

MySnapInDO

TestDataObject;

// The test data object.

bitmap.

IntPtr

BMap16;

// Handle for the

16

X

16

IntPtr

BMap32;

// Handle for the

32

X

32

bitmap.

// Create the TestDataObject. TestDataObject = (MySnapInDO)lpDataObject;

switch(aevent)

{

case MMC_NOTIFY_TYPE.MMCN_SHOW:

RESULTDATAITEM Item; // Result data variable.

//Create two headers. MMCHeader.InsertColumn(0, "Name Type", 0, 250); MMCHeader.InsertColumn(1, "Name Value", 0, 150);

//Initialize the result data variable.

Item = new RESULTDATAITEM();

//Create entries required for first item. Include

//constants for relevant items in mask. Make sure

//you use a callback for strings. Set the image to

//the 16 X 16 pixel image.

Item.mask = RDI.STR | RDI.IMAGE | RDI.PARAM;

Item.str = new IntPtr(-1); // MMC_CALLBACK

Item.nImage = 0;

Item.lParam = 0;

Item.nCol = 0;

//Display the first item. MMCResultData.InsertItem(ref Item);

//Modify lParam member for second query. Item.lParam = 1;

//Display the second item. MMCResultData.InsertItem(ref Item);

//Perform the same two steps for subsequent items. Item.lParam = 2;

MMCResultData.InsertItem(ref Item); Item.lParam = 3; MMCResultData.InsertItem(ref Item); Item.lParam = 4; MMCResultData.InsertItem(ref Item); Item.lParam = 5; MMCResultData.InsertItem(ref Item); Item.lParam = 6; MMCResultData.InsertItem(ref Item); Item.lParam = 7; MMCResultData.InsertItem(ref Item);

break;

case MMC_NOTIFY_TYPE.MMCN_EXPAND:

//Normally you'd place some code here for the

//scope pane, but we're not doing anything special

//with the scope pane for this example.

break;

case MMC_NOTIFY_TYPE.MMCN_ADD_IMAGES:

// Add

Images

Object

TempObj = arg;

IImageList ImageList = (IImageList)TempObj;

//Load bitmaps associated with the scope pane

//and add them to the image list

//Retrieve an embedded bitmap from the current assembly. Assembly Asm = Assembly.GetExecutingAssembly();

Stream Strm = Asm.GetManifestResourceStream("MMCSample.Main.bmp");

Bitmap Temp = new Bitmap(Strm);

// Create compatible bitmaps for the two bitmap images. IntPtr Translate = new Bitmap(Temp, 16, 16).GetHbitmap(); BMap16 = MMCFunctions.TranslateBitmap(Translate); WindowsFunctions.DeleteObject(Translate);

if (BMap16 != IntPtr.Zero)

{

//If the 16 X 16 bitmap worked, load a 32 X 32 bitmap as

//well.

Translate = new Bitmap(Temp, 32, 32).GetHbitmap();

BMap32 = MMCFunctions.TranslateBitmap(Translate);

WindowsFunctions.DeleteObject(Translate);

//Only if both bitmaps load successfully do we want to

//create the image list.

if (BMap32 != IntPtr.Zero)

{

int Result = ImageList.ImageListSetStrip(ref BMap16, ref BMap32, 0, 0x00FFFFFF);

if (Result != RESULT_VAL.HR_OK) return Result;

}

}

break;

default:

// The snap-in doesn't support any other messages. return RESULT_VAL.HR_FALSE;

}

return RESULT_VAL.HR_OK;

}

Responding to the MMC's request to show the component's data is straightforward. The code begins by adding two headers to the MMC display. It doesn't matter which view MMC is currently using; you'll definitely want the headers to be available if the user decides to use the Detail view rather than one of the other views like Large Icon. In fact, the Detail view is the only useful view for this component, since the other views hide the data and we haven't implemented a dialog that could be used to display the data instead.

Once the two column headings are displayed, the next step is to initialize the RESULTDATAITEM data structure variable, Item. Notice that this is accomplished in two steps. First, the code creates the object using a standard constructor. Then, the code fills in the data structure elements. A special data structure element named mask contains constants that indicate which of the other data structure elements contain information. In this case, we'll use the str, nImage, and lParam data members to hold information. You'll also initialize nCol to 0 to ensure the component starts displaying information at the right column.

Notice the str input value is MMC_CALLBACK (although we don't use the actual C++ constant in this case). The Microsoft documentation seems to say you could place a string in this data member if the output consists of a single column of data, rather than rely on a

callback function. However, it's safer and more flexible to rely on a callback function. You must use a callback function when displaying more than one column of data.

Adding the data to the MMC display comes next. You'll use the InsertItem() method to add new data items to the display. This method requires a single argument as input, a pointer to the item that you want to add, which must be a RESULTDATAITEM data structure. There are also methods for removing and modifying existing items should you wish to do so.

The rest of the code performs two tasks. First, it places a new value in lParam to indicate which kind of data to display. Second, it uses InsertItem() to add the item to the display.

The ATL COM AppWizard doesn't automatically provide a callback function to use for displaying the items on screen, so you'll need to add it. However, you must add the callback function, GetDisplayInfo(), to the class that implements the IComponent interface. Normally, this function appears in your main component class. However, this interface could appear anywhere (as witnessed by the Microsoft examples provided with products like MSDN). Since there isn't any consistent place for this interface to appear, you'll need to check your source code before adding the GetDisplayInfo() function.

Once you do find the class that implements the IComponent, you'll need to add the GetDisplayInfo() function to it. Listing C.4 shows the code you'll need to implement the new member function. Note that the listing only shows the code required for one result type—the application actually supports eight different types. However, the other cases work much the same as the first case shown.

Listing C.4: The GetDisplayInfo () Method Provides Data Output for the MMC Snap-In

public unsafe int GetDisplayInfo(ref RESULTDATAITEM ResultDataItem)

{

String

ComputerName;

//

Buffer to hold computer name data.

int

BufferSize = 15 + 1; //

Buffer size, plus a null.

String

Temp;

//

A temporary conversion buffer.

// If the caller sent some information. if (ResultDataItem != null)

// If that information contains a string.

if ((ResultDataItem.mask & RDI.STR) == RDI.STR)

{

// Check which string the caller is requesting. switch (ResultDataItem.lParam)

{

case 0:

// Display the NetBIOS name item. if (ResultDataItem.nCol == 0)

{

//Create the temporary data value, convert it

//to a pointer, and then pass it to MMC. Temp = "NetBIOS Name";

fixed (char* ResultData = Temp)

{

ResultDataItem.str = new IntPtr(ResultData);

}

}

// The requester is asking for the second column. else

{

// See if there is a NetBIOS name for this item. if (WindowsFunctions.GetComputerNameEx(

COMPUTER_NAME_FORMAT.ComputerNameNetBIOS, out ComputerName,

ref BufferSize))

//Output the contents of the string buffer. fixed (char* ResultData = ComputerName)

{

ResultDataItem.str = new IntPtr(ResultData);

}

//If not, display a failure string.

else

{

Temp = "Value Not Available"; fixed (char* ResultData = Temp)

{

ResultDataItem.str = new IntPtr(ResultData);

}

}

}

break;

}

}

// Return a success value. return RESULT_VAL.HR_OK;

}

While this code may appear long in the source file, it actually contains a few processing steps that get repeated over and over using different values, depending on the item that the caller wants to display. This example requires two variables, one of which is initialized to MAX_COMPUTERNAME_LENGTH + 1 (15 + 1 for a PC or 31 + 1 for a Macintosh). You need to initialize the buffer length to the maximum computer name string size and then add 1 for a null termination in order to ensure you can display the entire computer name. In addition, the Microsoft documentation warns that using a shorter string can have unexpected results under Windows 95.

The next task is to check whether the caller has provided a filled (or at least initialized) RESULTDATAITEM data structure. The code also verifies that the data structure contains an initialized str data element. The reason is simple: This component provides text output, so the str data element is absolutely essential. Figuring out what to display comes next; as previously stated, there are eight different computer name values that you can obtain using the GetComputerNameEx() function.

The processing sequence for all eight outputs is the same, so I'll only describe one of them. There are two columns for data output. The first column contains a string that shows what

type of computer name data we're displaying. Since this value is constant, we can simply place it into the str data element for immediate display. Notice the use of pointers (unsafe code) and fixed blocks in this example. Make sure you perform any required data conversions before you place a value in the data structure, or you'll get memory read errors when using MMC.

The second column contains the actual value for the computer name type that we want to display. This means using the GetComputerNameEx() function, which requires three inputs: a constant containing the type of name we want to retrieve, a pointer to a buffer that can accept the name, and size of the buffer provided. The code converts the string variable contents to a pointer for output.

Not every attempt to get a computer name is going to be successful. In most cases, there isn't a name to get. With this in mind, you'll need to provide an alternative string for the user. In this case, the component displays a simple, "Value Not Available" string.

Working with IDataObject

Every MMC snap-in must implement the IDataObject interface. However, you'll find the vast majority of the methods for IDataObject return a "not implemented" value (RESULT_VAL.HR_NOTIMPL in the example). You must tell MMC that these methods aren't implemented, so it knows that the snap-in answered the call and simply doesn't provide the required functionality.

In most cases, you'll need to implement the class constructor in order to initialize data values. The data values will generally include the clipboard formats the class will handle and a private variable to hold the snap-in node. The snap-in node is especially important because it provides a context for all other activities performed in the class.

You'll also need to implement the GetDataHere() method to perform data conversions within the snap-in. This method contains a wealth of unsafe code—code that requires use of pointers and use of system memory that the Garbage Collector can't move. Consequently, you need to mark this method as unsafe and compile the snap-in using the /Unsafe command-line switch like this:

public unsafe int GetDataHere(ref FORMATETC pFormatEtc, ref STGMEDIUM pMedium)

The GetDataHere() method uses some of the same techniques as other parts of the example. For example, we need to create a data buffer using the Windows CreateStreamOnHGlobal() API call. If this call fails, you'll need to throw an exception indicating the failure and exit the snap-in.

The main purpose of the method is to provide certain types of output data, including the node's display name and the GUID for both the snap-in and node. Typically, performing this task requires that the method place some data into the data buffer, create a data stream, and write the data out to the requestor. Here's an example of this activity.

//Determine if we need to output a name string for this node. The

//example doesn't support the zero deliminated node type, so send

//nothing in that case.

if (ClipFormat == CBDisplayName || ClipFormat == CBSZNodeType)

{

//Assign a value to the data buffer. if (ClipFormat == CBDisplayName)

DataBuffer = TheNodeData.DisplayName; else

DataBuffer = Nothing;

//Determine the data buffer length. DataLength = DataBuffer.Length;

//Write the data to the data stream. Notice that you need to

//create a fixed block in order to perform the write. This

//ensures the snap-in writes the data without interference from

//the garbage collector.

fixed(byte* pData = DataBuffer)

{

DataStream.Write((IntPtr)pData, (uint)DataLength, out DataSent);

}

}

As you can see, the code detects the requested clipboard format. If the format is the correct type, it assigns a value to DataBuffer. Once the data is in place, we also need to obtain the length of the data and place it in DataLength. The final step is to write the data using the DataStream.Write() method. This part of the task occurs within a fixed block to ensure the Garbage Collector doesn't attempt to collect the DataBuffer or move the memory in some way.

Testing the Snap-In within MMC

At this point, you're ready to compile the MMC snap-in and place it on the target server. The following procedure will help you test the MMCSample component.

Note You must register both the MMCSample.DLL and the MMCHelper.DLL using the RegAsm and GACUtil utilities. Otherwise, CLR won't find everything it needs to display the MMC snap-in. The error you receive when this happens is that MMC couldn't instantiate the object. Rather than spend hours checking your code or the Registry entries by hand, simply ensure that you have registered both DLLs.

1.Start MMC using the Start Run command. Type MMC in the Open field and click OK. You'll see a blank MMC window.

2.Use the Console Add/Remove Snap-In command to display the Add/Remove SnapIn dialog.

3.Click Add. You'll see an Add Standalone Snap-In dialog. Look for the proper name, icon, provider information, and version when you view the component. All of these items are provided by the ISnapinAbout interface.

4.Click Add, then Close. You'll see the new snap-in added to the Add/Remove Snap-in dialog. Highlight the snap-in and you'll see text added to the Description field.

5.Click OK. You'll see the CompName component added to the Component Services display. Figure C.7 shows an example of this component in action.