Thursday, March 13, 2008

I pulled the trigger on a new desktop setup which is out for delivery according to the internets.  Because of my recent research I went ahead and went the quad core route, and after reading Scott Hanselman's blog post about going x64 being a complete non issue I'll be giving Vista x64 a try.  I also got a 1080p monitor and a Blu-Ray drive; probably slightly overkill but why not.  It's difficult to explain to non-programmers, but an appropriate setup with the right desk space, music, multiple monitors, and a repsonsive machine can make developing software a joy.  It can be a joy without a Raptor drive and a 24" display but these things help. 

I got a decent enough video card to check out some of the recent games, Bioshock being at the top of the list.

Thursday, March 13, 2008 8:19:39 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, March 07, 2008

Catastrophic failure!  Now that is an error message worthy of my college dating life!... anyhoo, I digress.  Many controls in winforms and WPF probably try to do things in the constructor or an "OnLoad" event of some kind that are not design-time-friendly, but the magic of the environment mostly keeps things like this from happening.  This magic dust has not yet been sprinkled upon the Silverlight 2.0 tools for VS2008, but I'm still very excited about the possibilities as this platform matures.  The error below occurs if I add some test images into the loaded event of the carousel and try to view the designer. 

Friday, March 07, 2008 8:23:31 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback

Here's my carousel sample, as a user control within my Silverlight image uploader. The Tesla Roadster is such a sexy car it's perfect for any automotive industry technology demonstration.

Friday, March 07, 2008 8:07:43 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback

Ok, here is my silverlight 2 port of the ever popular Image Carousel sample.  For me, at least, it's much easier to follow what's going on than the JavaScript version.  Here's the markup required in the page:

<UserControl x:Class="ImageUploader.ImageCarousel"

    xmlns="http://schemas.microsoft.com/client/2007"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Width="800" Height="600" Canvas.Top="0">   

<Canvas x:Name="_mainCanvas" Loaded="mainCanvasLoaded" MouseMove="whenMouseMoves"

        MouseLeftButtonDown="mainDown" MouseLeftButtonUp="mainUp" MouseLeave="mainCanvasMouseLeave" >

    <Canvas Name="imagesHolder" Canvas.Left="0" Canvas.Top="0">

        <Canvas Name="mainImageHolder" Canvas.Left="200" Canvas.Top="60" Canvas.ZIndex="169" Opacity="1.0">

            <Image Name="mainImage" Stretch="UniformToFill" Height="225" Width="300"/>

        </Canvas>

    </Canvas>

</Canvas>

</UserControl>

And here is the code-behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Media.Imaging;

namespace ImageUploader
{
public partial class ImageCarousel : UserControl
{
public ImageCarousel()
{
InitializeComponent();
_mainDown = false;
_urls = new Dictionary<Image, string>();
_angles = new List<double>();
_imagePaths = new List<string>();
//_imagePaths.Add("Upload/1.jpg");
//_imagePaths.Add("Upload/2.jpg");
//_imagePaths.Add("Upload/3.jpg");
_dispTimer = new System.Windows.Threading.DispatcherTimer();
_dispTimer.Interval = TimeSpan.FromMilliseconds(25);
_dispTimer.Tick += new EventHandler(dt_Tick);
_dispTimer.Stop();
}

private List<double> _angles;
private bool _mainDown = false;
private List<string> _imagePaths;
private List<ScaleTransform> _scaleTransforms = new List<ScaleTransform>();
private double _speed = -0.0425;
private Dictionary<Image, string> _urls;
private System.Windows.Threading.DispatcherTimer _dispTimer;

public void AddImage(string imageUrl)
{
_mainDown = false;
_dispTimer.Stop();
_angles.Clear();
_scaleTransforms.Clear();
_urls.Clear();

for (int i = 0; i < _imagePaths.Count; ++i)
{
Canvas holder = (Canvas)_mainCanvas.FindName("imgHolder" + i);
_mainCanvas.Children.Remove(holder);
}

_imagePaths.Add(imageUrl);
Image img = (Image)_mainCanvas.FindName("mainImage");
string srcPath = _imagePaths[_imagePaths.Count -1 ];
img.Source = new BitmapImage(new Uri(GetURLBase() + srcPath));
img.SetValue(Canvas.ZIndexProperty, 169);

BuildImages();
}

private void mainCanvasLoaded(object sender, RoutedEventArgs e)
{
if (_imagePaths.Count > 0)
{
Image img = (Image)_mainCanvas.FindName("mainImage");
img.Source = new BitmapImage(new Uri(GetURLBase() + _imagePaths[0]));
img.SetValue(Canvas.ZIndexProperty, 169);
//
BuildImages();
}
}

public List<string> ServerImagePaths { get; set; }

protected void BuildImages()
{
int leftPos = 0;
for (int i = 0; i < _imagePaths.Count; ++i)
{
Canvas imgHolder = new Canvas();
imgHolder.SetValue(Canvas.NameProperty, "imgHolder" + i);
imgHolder.SetValue(Canvas.LeftProperty, leftPos);
imgHolder.SetValue(Canvas.TopProperty, 0);
//Come back to reflections...
//Canvas imgHolderReflection = new Canvas();
//imgHolderReflection.SetValue(Canvas.NameProperty, "imgHolderRef" + i);
//imgHolderReflection.SetValue(Canvas.TopProperty, 160);
//imgHolderReflection.Opacity = 1;
//LinearGradientBrush lgb = new LinearGradientBrush();
//lgb.StartPoint = new Point(0, 0);
//lgb.EndPoint = new Point(0, 1);
//lgb.GradientStops.Add(new GradientStop());
//lgb.GradientStops.Add(new GradientStop());
//lgb.GradientStops[0].Offset = .37;
//lgb.GradientStops[0].Color = Color.FromArgb(0x00, 0x00, 0x00, 0x00);
//lgb.GradientStops[1].Offset = 1;
//lgb.GradientStops[1].Color = Color.FromArgb(0x88, 0x00, 0x00, 0x00);
//imgHolderReflection.OpacityMask = lgb;
//imgHolder.Children.Add(imgHolderReflection);
//
Rectangle rec = new Rectangle();
rec.SetValue(Rectangle.NameProperty, "rec" + i);
rec.SetValue(Canvas.TopProperty, -6);
rec.SetValue(Canvas.LeftProperty, -6);
rec.Height = 92.0;
rec.Width = 92.0;
rec.Fill = new SolidColorBrush(Colors.LightGray);
imgHolder.Children.Add(rec);
//
Image img = new Image();
img.Cursor = Cursors.Hand;
img.MouseEnter +=new MouseEventHandler(img_MouseEnter);
img.MouseLeave +=new MouseEventHandler(img_MouseLeave);
img.MouseLeftButtonDown +=new MouseButtonEventHandler(img_MouseLeftButtonDown);
img.MouseLeftButtonUp +=new MouseButtonEventHandler(img_MouseLeftButtonUp);
img.Stretch = Stretch.UniformToFill;
img.SetValue(Image.NameProperty, "img" + i);
BitmapImage imgSource = new BitmapImage(new Uri(GetURLBase() + _imagePaths[i]));
_urls.Add(img, GetURLBase() + _imagePaths[i]);
img.Source = imgSource;
img.Width = 300;
img.Height = 225;

img.Opacity = 1;
imgHolder.Children.Add(img);
ScaleTransform imgCanvasScaleTrans = new ScaleTransform();
_scaleTransforms.Add(imgCanvasScaleTrans);
imgCanvasScaleTrans.ScaleX = 1;
imgCanvasScaleTrans.ScaleY = 1;
imgCanvasScaleTrans.CenterX = 50;
imgCanvasScaleTrans.CenterY = 50;
imgHolder.RenderTransform = imgCanvasScaleTrans;
//
_mainCanvas.Children.Add(imgHolder);
//
_angles.Add( i*((Math.PI*2)/_imagePaths.Count) );
}
PositionItems();
_mainDown = true;// huh?
//Set dispatcher interval
_dispTimer.Start();
}

/// <summary>
/// Not used yet
/// </summary>
/// <param name="i"></param>
protected void AddOne(int i)
{
}

void dt_Tick(object sender, EventArgs e)
{
MoveItems();
}

protected void PositionItems()
{
int radiusX=400;
int radiusY=110;
int centerX=425;
int centerY = 260;

for(int i = 0; i < _imagePaths.Count;++i)
{
double myX = Math.Cos(_angles[i])*radiusX + centerX;
double myY = Math.Sin(_angles[i])*radiusY + centerY;
Canvas imgCanvas = (Canvas)_mainCanvas.FindName("imgHolder" + i);
imgCanvas.SetValue(Canvas.LeftProperty, myX);
imgCanvas.SetValue(Canvas.TopProperty, myY);
ScaleTransform stRef = _scaleTransforms[i];
double sc = (myY - stRef.ScaleY) / (centerY + radiusY-stRef.ScaleY);
stRef.ScaleX = sc;
stRef.ScaleY = sc;
_angles[i] += _speed;
imgCanvas.SetValue(Canvas.ZIndexProperty, (int)myY);
}
}

protected void MoveItems()
{
if (_mainDown)
{
PositionItems();
}
}

void img_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{

}

void img_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Image img = (Image)sender;

     Image mainImg = (Image)_mainCanvas.FindName("mainImage");
mainImg.Source = new BitmapImage(new Uri(_urls[img]));
mainImg.SetValue(Canvas.ZIndexProperty, 169);
}

void img_MouseLeave(object sender, MouseEventArgs e)
{
//Change color BACK or something cool
}

void img_MouseEnter(object sender, MouseEventArgs e)
{
//Change color or something cool
}

private void whenMouseMoves(object sender, MouseEventArgs e)
{
Point pt = e.GetPosition(null);
double _root_xmouse = pt.X;
     double _root_ymouse = pt.Y    ;
_speed = ((_root_xmouse - 500) / 500) * 0.0755;
}

private void mainDown(object sender, MouseButtonEventArgs e)
{
_mainDown = true;
}

private void mainUp(object sender, MouseButtonEventArgs e)
{
_mainDown = false;

}

private void mainCanvasMouseLeave(object sender, MouseEventArgs e)
{

}


protected string GetURLBase()
{
string str = System.Windows.Application.Current.Host.Source.OriginalString;
return str.Substring(0, str.LastIndexOf("/")).Replace("ClientBin", string.Empty);
}
}
}


 

Not formatted as well as my last posts, I know.  It seems that Google reader doesn't like it when I paste out of Word. Now, one thing that is not very apparent is how to add this UserControl to another UserControl, assuming that JUST having an image carousel is not what you're after.  You need to include the following on the top-level user control:

xmlns:CarSpot="clr-namespace:ImageUploader"

which then allows you to refer to the code:

<CarSpot:ImageCarousel x:Name="_carousel"></CarSpot:ImageCarousel>

This is an aspect of XAML I don't like. There's nothing (that I can see) in the structure/XSD of a XAML document that tells me that an attribute value starting with "clr-namespace:" is something special.  There's a lot of magic like this in XAML.  The Visual Studio "Silverlight Chainer" as the file calls the Beta 1 tools are definately buggy and incomplete.  I am lucky to be able to hit F5 10 times before I get errors trying to debug and having to kill Cassini and VS2008.  I've also found that it's very easy to crash the designer by having naughty things in your code-behind.  It appears that unlike more mature design-time experiences there is no "IsDesignTime" flag that would allow you to keep from having code execute while designing.  I can tell that this is Cider by the wonderfully descriptive stack traces I get, yet things like dragging the new Silverlight controls onto the surface or even moving them around (I'm sick of tweaking location with Canvas.Top/Canvas.Left) are not working yet.  This is a huge step forward, but I've got to think there'll be another beta after this one now.

And that's that.  I should have some more Silverlight 2 observations later, especially about the threading and HTTP aspects.

Friday, March 07, 2008 4:36:07 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback

I have translated the Silverlight "Image Carousel" into Silverlight 2.0 and C# rather than the "JS writing XAML" model.  If I get the code cleaned up this afternoon I will post the carousel portion.

Friday, March 07, 2008 1:33:00 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, March 05, 2008
IE8

I am blogging from IE8.

Many useful things are broken, such as FreeTextBox which I am used to using to write this article.

Also broken, ironically enough, are the dynamic menus generated by asp.net from a sitemap data source. That alone will force me into "Emulate IE7" mode until Beta 2...

Wednesday, March 05, 2008 4:06:34 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Monday, March 03, 2008

I have never been more ready for spring than I am this year.

It was a whopping 40degrees here yesterday and I took advantage of it.  I spent hours chopping ice out of my driveway, I can't type so well today due to the blisters on my fingertips.  The huge piles of snow on either side of my driveway make surprisingly good reach-level beer holding areas, and I pulled out the bbq pit and made some brats.

I also wrote a Mapping tool this weekend.  When you're first getting started it's interesting to note the similarities in flat file, XML, object-to-object, and SQL to Object mapping: at least in terms of the initial abstractions that present themselves.  I specifically wanted to solve a flat-file to complex object problem and write a tool that previews the mapping in real time and realized I couldd do quite a bit more if I weren't lazy.  The main place I could see using this in my own efforts are class-to-class mapping.  For example: if you are consuming a web service the proxy generated will return to you class instances using whatever naming conventions and object structure the designers of said service wanted to give you.  Even if another team in your organization is the maintainer of this service, Directly using these proxy types within your code is probably a mistake; mapping ProxyObject.PrimaryKey to MyDomainObject.Id quickly becomes tedious.  A mapping tool that does the annoying work for you and saves the mapping as an easy XML file or better yet generates transformation code might be helpful. 

Monday, March 03, 2008 9:50:15 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Thursday, February 28, 2008

I downloaded the update package and installed from that, no luck.  I started searching for other people with the same problem and found that it is quite common.  I passed over some of the insane "fixes" in favor of trying some of the easier approaches.  Apparently, the Zune installer requires that the Windows Firewall service be running during installation.  What a bizarre dependency.  I'm all Zuned up now and liking it.  Now my wife wants one.

Thursday, February 28, 2008 9:50:53 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, February 27, 2008

Got the Zune today.  So far I am underwhelmed.  My very fast and very up to date Vista Laptop failed to install the Zune software the first time.  The error code explains this could be one of a number of thigns: .net 2, encoders and a lot of other developer-sounding nonsense.  Since the Zune installer checks for updates etc. this is unacceptable.  I am directed to a page with a svelte 173mb download to get all of the correct versions at once. 

Downloading...

.NET | Rant
Wednesday, February 27, 2008 2:37:04 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Tuesday, February 26, 2008

Invoking a .NET program on Device Insert

Given the popularity of the .NET Platform it seems odd that Microsoft does not provide easier means of doing certain incredibly useful and easy to imagine tasks from the .NET framework.  It took what seems like a ridiculous amount of research to accomplish a simple task:

Lots of programs come up as ‘launch’ options when I insert a storage card/camera/video camera/PDA/Mp3Player/younameit device into a USB port.  I’d like my .NET program to be in that list and to run if the user selects it.

File Types & WIA

The most useful thing you get in Visual Studio towards this goal is file types.  You can build an MSI for your desktop application and make your “.damon” files open with this program and have right clickàOpen appear in the shell context menu for “.damon” files.  I’ve written before about how to do multiple-select and some useless and goofy things with this scenario, but this doesn’t get us anywhere near where we want to be. 

WIA is very useful for media-specific actions like this, and handles the “something was plugged in” scenario in specific cases.  There is a WIA Automation Layer and managed wrappers for this.  Obviously if you care about something besides JPGs and AVIs WIA may not work for you.  Also, WIA requires that the device you’re speaking to has a WIA driver.  WIA drivers are certainly very common for the appropriate device types, but when you are distributing desktop software that depends on an OEM’s drivers you’re stepping into a world of pain (Not a world of Payne, which is actually a wonderful place).    Can the OEM give you merge-modules for the install?  Are their drivers buggy?  Be sure to treat your Support Group very well if you go down this path.

The Problem Solution: Shell Extensions and Managed Code

I’m going to focus on a simple subset of the overall problem.  Despite the perceived simplicity of the final solution, I couldn’t find a single working example of this online so if you got here via Goolge you’ve hit the jackpot.   Suppose you have an SD card, an SD card reader, and some family photos on said SD card.  You would like a .NET program to automatically create thumbnails of these when the SD card is inserted into your card reader.

Step 1

Step 1 is to read some obscure documentation found here: http://msdn.microsoft.com/msdnmag/issues/01/11/autoplay/

The first part of this is easy to follow. The Windows Autoplay functionality mostly resides in the sub keys of HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\AutoplayHandlers\EventHandlers\

Oddly enough, the names of these sub-keys are almost meaningful and you can do some Googling Windows Live Searching to get an idea of what exactly will cause this or that handler to be invoked.  In our case, the key we want for XP and Vista is ShowPicturesOnArrival.  Windows scans the SD card after it’s inserted and finds JPGs or whatever and checks this sub key to determine what choices the Shell should offer the user.  What can be gleaned from the MSDN article in Step 1 is that we can make up a Handler name, so I’ll call mine TestPhotoHandler.  This “TestPhotoHandler” must correspond to a sub key of HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\AutoplayHandlers\Handlers (../../Handlers from ShowPicturesOnArrival), which will in turn contain some values that describe what TestPhotoHandler is to the Windows Shell.

Step 2

Step two is to populate our Handler.

·         Action=what will appear to the user they are doing with this handler. 

·         DefaultIcon=Pretty self explanatory.  You can also use the Foo.exe,-1 syntax to specify an embedded resource. 

·         Provider=Name of your program

·         InvokeProgID, InvokeVerb=the interesting part, keep reading.

o   Note that it would be FAR too easy to just let you include the path to your .exe or .dll here…

With just the above work done, you can see the following behavior in Windows:

Step 3

Telling Windows where to go from here is far more difficult than it needs to be, and I could not find a single working example of getting this stuff to work with managed code.  In addition to telling Windows how to find our .NET program, Windows must be able to communicate with our program.  There are three interfaces that are interesting for these purposes:

1.       IHWEventHandler

2.       IHWEventHandler2

3.       IDropTarget

These are COM interfaces.  Because I keep meaning to get back into C++ development I always install C++ accessories in Visual Studio.  This means I have the various COM header and IDL files installed, and I can dig up some useful information.  Recall that .NET classes can implement COM interfaces as long as you know the GUID of the COM interface.  Here, then, is my IDropTarget interface definition for .NET objects.

    [ComImport,

     InterfaceType(ComInterfaceType.InterfaceIsIUnknown),

     Guid("00000122-0000-0000-C000-000000000046")]

    public interface IDropTarget

    {

        [PreserveSig()]

        int DragEnter(IntPtr pDataObj,               //  IDataObject

                      ulong grfKeyState,             //  DWORD

                      POINTL pt,

                      ref ulong pdwEffect);          //  DWORD*

 

        [PreserveSig()]

        int DragOver(ulong grfKeyState,              //  DWORD

                     POINTL pt,

                     ref ulong pdwEffect);           //  DWORD*

 

        [PreserveSig()]

        int DragLeave();

 

        [PreserveSig()]

        int Drop(IntPtr pDataObj,                    //  IDataObject

                 ulong grfKeyState,                  //  DWORD

                 POINTL pt,

                 ref ulong pdwEffect);               //  DWORD*

 

    }

 

    [StructLayout(LayoutKind.Sequential)]

    public struct POINTL

    {

        public POINTL(long xx, long yy) { x = xx; y = yy; }

        public long x;

        public long y;

        public override string ToString()

        {

            String s = String.Format("({0},{1})", x, y);

            return s;

        }

    }

 

PointL was copied from Dino Esposito’s excellent article on Shell Extensions on The Server Side.  At any rate, these three attributes on IDropTarget basically tells COM that we can speak the COM lingo and have the behaviors specified by the Interface identified by this GUID.

Step 4

Next we’ll implement the IDropTarget Interface.  The implementation can supposedly reside either in an .exe or .dll, but for our purposes it only works and is only safe when the implementation is on the Entry Point of an .exe project.  So,

    [Guid("C1D0D2F6-5F45-4462-9835-C463AFE367E3")]

    [ComVisible(true)]

    public class Program : IDropTarget

    {

        /// <summary>

        /// The main entry point for the application.

        /// </summary>

        static void Main()

        {

There are some things to take note of here.  Since we wish to enter the world of COM (and COM is love…) we put a ComVisible attribute on the class.  The default Program created for you in VS2008 has Program as a static class, which cannot have attributes so we’ve removed that.  We also have used the Tools in Visual Studio to create a GUID for this class.  We are using an API known as “HackingtheRegistrySinceNoRealRegistrationAPIExists” and guids are the coin of the realm.  I happen to hate this API but options are scarce.

Step 5

The next step is to register our code in such a way that it is findable via the appropriate magic.  For .NET programs RegAsm.exe is our registrar of choice.  Note that RegAsm will pout if your Assembly is not strongly named.  If we now “Regasm.exe /codebase LaunchTestProg.exe” we get some interesting registry settings created for us.  Under HKEY_CLASSES_ROOT\CLSID we find our Program’s GUID, along with the following:

A sub key named “ProgId” containing the fully qualified class name of the .NET program we’d like to run is nestled beneath our class id guid.  This is part of the explanation for the values in Step 2.  Continuing to search for our GUID in the registry we find that HKEY_CLASSES_ROOT\LaunchTestProg.Program has been created for us. 

Step 6

Rather than be clever I’ll just show the final piece we need to create.  The Shell depends on various lookups and naming conventions to determine what it needs to do. 

Below HKEY_CLASSES_ROOT\LaunchTestProg.Program, we need to create some registry keys and values ourselves.  We create sub keys shell, open, DropTarget, and put the GUID of our .NET program as a clsid attribute of the DropTarget key.  The entire process can now ask the user, find, and Invoke our .NET program when a USB device is inserted and it meets the original Handler criteria.

Further Investigation

Upon testing this, I found some interesting behaviors.  It appears that while the .NET project must be an .exe, what Windows is really doing is instantiating the Program object and invoking the members of IDropTarget on it.  I have not tried too hard to get Main to run.  In normal cases, IDropTarget.DragEnter and then IDropTarget.Drop will be called.  For my particular needs, the arguments to either method are throw-away, but using these should be normal P/Invoke & Marshalling exercises.  If you just want to start some other .NET code you can now do so from the your Drop()  implementation.

I will also warn that you want to read this and understand what it means: http://blogs.msdn.com/junfeng/archive/2005/11/18/494572.aspx

I will also warn that the class library Assembly that contains my IDropTarget interface seems to be file-locked once I actually invoke my program.  Restarting Explorer.exe fixes this, which may be a consideration if you are going to be auto-updating the program in some way.  It seems odd that the .exe containing the actual program is not locked.  I plan on investigating the registry areas of Windows\CurrentVersion\ShellExt\Cached to see if clearing that and firing the appropriate Registry Changed event will unlock the assembly. 

Other Avenues...

You will notice DeviceGroups and DeviceHandlers keys as siblings of the “EventHandlers” subkey.  I also mentioned IHWEventHandler and IHWEventHandler2.  The latter is merely the Vista version of the former that adds an extra method, presumably for UAC.  It seems that allowing a .NET program to reconfigure itself if a new input device is plugged in is certainly not out of the range of possibilities.

Would you like some thrown-together code that does all of this for you?  I’ve been meaning to get better about posting more code, so here we are:

    public class DropTargetInstaller

    {

 

        public bool Install(Assembly exe, string entryPointTypeName, string desiredHandlerName, string iconPath, string actionText, string usingText)

        {           

            Type<