I've been working on a new internal portal for my employer using SharePoint 2010. As the project progressed, we experimented with several custom page layouts and, at length, settled on our two favorites.

With the decision final, we reassigned all the pages to the authorized page layouts and proceeded to delete the extras... all except one.

This page layout didn't want to get deleted. I was persistent, but the file was more so.

 

  • Deleting the file in SharePoint Designer yielded the error "Server error: This item cannot be deleted because it is still referenced by other pages." Mind you, it wasn't, but SharePoint thought it was.
  • Several online sources indicated that moving it to a sub-folder should allow deletion of the folder. However, in my case, doing so in SharePoint Designer yielded the error "Server error: This folder cannot be deleted because it contains the following page layout that is still in use: LayoutPage01.aspx."
  • I could open the master page gallery in Windows Explorer, attempt to delete the file, or move it to a folder and delete the folder, and yet the file remained.
  • I fired up PowerShell and attempted to delete the file with code, and received the same error.
At this point I didn't quite know what to do. As a matter of fact, poking around the code with PowerShell didn't help me root out the source of the error; or, at least, not at first.

I was working on another task--one related to event receivers--when I noticed the following event receiver attached to the master page gallery:
Type                        : ItemDeleting
SequenceNumber              : 1000
Assembly                    : Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c
Class                       : Microsoft.SharePoint.Publishing.Internal.BlockDeletionOfInUseItemsEventReceiver
"Well, well," I thought. This must be the source of the error.

Upon inspection, I learned about the BackwardLinks property of the SPFile class. This collection provides information as to *which* files still lay claim on the page layout.

I took a look at the BackwardLinks collection on my particular page layout in PowerShell, and discovered four backward links, all from files that didn't exist anymore!

Well, enough story. Here is the PowerShell I used to resolve the problem. It will probably break your system and destroy all that you love... Don't say I didn't warn you, if you try to use it. The steps are simple:
  1. Find the layout in the gallery.
  2. Check to see if any of the BackwardLinks are to files that really exist.
  3. If non of the files exist, remove the event receiver that prevents deletion, delete the file, and then re-register the event receiver.
Happy Coding!

function Remove-PageLayout($siteUrl, $layoutName) {
  $site       = Get-SPSite $siteUrl
  $web        = $site.RootWeb
  $gallery    = $web.lists["Master Page Gallery"]
  $layoutItem = $gallery.Items |? { $_.Name -eq $layoutName }
  if(!$layoutItem) { throw (new-object IO.FileNotFoundException) }

  $layoutFile = $layoutItem.File

  $layoutFile.BackwardLinks |% {
    if(Test-Page $_.Url) {
      throw "Cannot delete page because it is referenced by $($_.Url)."
    }
  }

  $receiverType     = [Microsoft.SharePoint.Publishing.Internal.BlockDeletionOfInUseItemsEventReceiver]
  $unregisterMethod = $receiverType.GetMethod("Unregister", [reflection.bindingflags]"Static,NonPublic")
  $registerMethod   = $receiverType.GetMethod("RegisterIfNeeded", [reflection.bindingflags]"Static,NonPublic")

  $unregisterMethod.Invoke($null, $gallery)
  $layoutItem.Delete()
  $registerMethod.Invoke($null, $gallery)

  $site.Dispose();
}

function Test-Page($url) {

  $site = $web = $null

  trap [exception] {
    if($web) { $web.dispose() }
    if($site) { $site.dispose() }
    return $false
  }

  $site = new-object Microsoft.SharePoint.SPSite($url)
  $web = $site.openweb()

  $file = $web.GetFile($url)
  $result = $file.exists

  $web.dispose()
  $site.dispose()
  return $result
}

 
Categories: PowerShell | SharePoint

I ran into quite the mystery while building custom workflow activities for SharePoint 2010. It seemed that, no matter what I tried, the new activities would not appear in SharePoint Designer.

At length, I realized that the assembly name did not get resolved in the .actions file. Visual studio left the token $SharePoint.Project.AssemblyFullName$ as-is.

I found a solution on MSDN, which indicated the Visual Studio tools only replace SharePoint tokens on a few file extensions. To register additional file extensions, such as .actions, edit the project file and add the following at the end of the first <PropertyGroup> element.

<TokenReplacementFileExtensions>myextension;yourextension</TokenReplacementFileExtensions>

That did it for me. From that point forward, my workflow activity appeared in SharePoint Designer 2010 as expected.

Cheers!


 
Categories: SharePoint | Workflow

While working on a Silverlight business application, I was startled at the proliferation of value converters within my project. It started out innocent enough. One class named BooleanToVisibilityConverter, and its cousin BooleanInverseToVisibilityConverter. Eventually, there were IValueConverters implementations for entirely non-reuable situations, such as MyEntityCollectionToBrushBasedOnSomeDetailedCriteraConverter.

That's when this idea struck me. To avoid the need to manage a limitless library of value converters, why not create an event-driven value converter that delegates the conversion to a listener, such as a user control or page. Implementing the solution turned out to be fairly simple.


First, I created an EventArgs derivative to carry the converter's method arguments and collect the result, like so:

public class ValueConvertingEventArgs : EventArgs
{
   
public ValueConvertingEventArgs(
       
object value,
       
Type targetType,
       
object parameter,
       
CultureInfo culture)
   
{
       
this.Value = value;
       
this.TargetType = targetType;
       
this.Parameter = parameter;
       
this.Culture = culture;
   
}

   
public object Value { get; private set; }
   
public Type TargetType { get; private set; }
   
public object Parameter { get; private set; }
   
public CultureInfo Culture { get; private set; }
   
public object Result { get; set; }
}

Next, I implemented an IValueConverter that "forwards" the Convert and ConvertBack handling to event listeners, as shown below:

public class DelegatedValueConverter : IValueConverter
{
   
public event EventHandler<ValueConvertingEventArgs> Converting;
   
public event EventHandler<ValueConvertingEventArgs> ConvertingBack;

   
public object Convert(
       
object value,
       
Type targetType,
       
object parameter,
       
CultureInfo culture)
   
{
       
if (DesignerProperties.IsInDesignTool)
        return null;

        var handler = this.Converting;
       
if (handler == null)
            throw new InvalidOperationException(
"Subscription to 'Converting' is required.");

       
var args = new ValueConvertingEventArgs(
value
,
 targetType
,
 parameter
,
 culture
);
        handler
(this, args);
       
return args.Result;
    }

   
public object ConvertBack(
       
object value,
       
Type targetType,
       
object parameter,
       
CultureInfo culture)
   
{
       
if (DesignerProperties.IsInDesignTool)
        return null;

        var handler = this.ConvertingBack;
       
if (handler == null)
            throw new InvalidOperationException(
"Subscription to 'ConvertingBack' is required.");

       
var args = new ValueConvertingEventArgs(
value,
 targetType,
 parameter,
 culture);
        handler
(this, args);
       
return args.Result;
    }
}

This implementation throws an InvalidOperationException if the related event does not have any subscribers. After the event executes, the method returns the value of the Result property on the event arguments. The snippets below show the value converter in action:

XAML:

<navigation:Page.Resources>
   
<local:DelegatedValueConverter
       
x:Key="DelegatedConverter"
       
Converting="OnConverting" />
</navigation:Page.Resources>

...

<TextBlock Text={Binding Stuff, Converter={StaticResource DelegatedConverter}} />

C# Code-Behind:

private void OnConverting(object sender, ValueConvertingEventArgs e)
{
    e
.Result = string.Format(e.Culture, "Hello, {0}", e.Value);
}

With the DelegatedValueConverter in tow, I was able to reduce the overall number of IValueConverter implementations in my project. The new converter allowed the page to take responsibility for conversions each time it seemed appropriate. I still make new value converter classes if they seem to have general reusability; however, for one-offs or page-specific conversions, this approach meets the conversion need without flooding the assembly with converter classes.

Happy Coding!


 
Categories: C# | Silverlight

Background

I came across an interesting question recently. How does one execute a PowerShell script in a Visual Studio project's pre- or post-build event, and get precise error message feedback when errors occur.

Part of the problem is that errors seem to get "swallowed" in the PowerShell script. Output can be directed to the console as the post-build event occurs, but nothing gets sent to the error window, and the build is not interrupted on error.

My initial inclination was to simply use PowerShell error trapping and exit() codes in the PowerShell script to classify errors, but that hardly pleases the imagination. At least it provides build termination if you call exit() with any non-zero value, but it lacks precision.

When compilation errors occur in C# code, Visual Studio provides line numbers and column positions of the error, with the handly double-click jump-to-error goodness that makes tracking down the error at least a little less tedious. Using exit() codes does not provide the same quality of service for PowerShell scripts.

So, in an attempt to find the grail, I developed a small MSBuild task for executing PowerShell scripts and logging their errors for display in Visual studio. Here is how it was done.

Sample Project: Noc.PsBuild.zip (9.35 KB)

Creating The MSBuild Task

(The sample project uses Visual Studio 2008 and targets the .NET Framework 3.5. I believe the solution can be adapted forward to VS 2010 and backward to VS 2005. The MSBuild task also requires PowerShell 2.0.)

The project references the following assemblies for MSBuild and for PowerShell automation:

  • Microsoft.Build.Framework (GAC)
  • Microsoft.Build.Utilities.v3.5 (GAC)
  • System.Management.Automation (from C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0)

Executing a PowerShell script from an MSBuild task turned out to be fairly easy. I started with a task skeleton that includes a property to specify the path to the PowerShell script.

using System.IO;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class PsBuildTask : Task
{
   
[Required]
   
public string ScriptPath { get; set; }

   
public override bool Execute()
   
{
       
// ...
   
}
}

The class inherits from Task, which provides all the infrastructure for generating compiler errors and warnings via the Log property. The Execute() method should return false if the task is considered to fail. In this case, it should return false if a script contains errors. All that remains is to fire up the PowerShell runtime and execute the specified script.

The following code snippet starts the PowerShell runtime, adds a dot-sourcing command to the pipeline, executes the pipeline, and retrieves the errors.

// create Powershell runspace
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace
.Open();

// create a pipeline and feed it the script text
Pipeline pipeline = runspace.CreatePipeline();
pipeline
.Commands.AddScript(". " + ScriptPath);

// execute the script and extract errors
pipeline
.Invoke();
var errors = pipeline.Error;

The errors object can read errors from the pipeline, if they exist, using one of the Read() overloads. The next snippet iterates through the errors, if any, then retrieves the error record and logs to the Log utility. This particular LogError() overload allows the specification of which file, line, and column is responsible for the error. That is key to getting the double-click send-me-to-the-error behavior in Visual Studio.

// log an MSBuild error for each error.
foreach (PSObject error in errors.Read(errors.Count))
{
   
var invocationInfo = ((ErrorRecord)(error.BaseObject)).InvocationInfo;
   
Log.LogError(
       
"Script",
       
string.Empty,
       
string.Empty,
       
new FileInfo(ScriptPath).FullName,
        invocationInfo
.ScriptLineNumber,
        invocationInfo
.OffsetInLine,
       
0,
       
0,
        error
.ToString());
}

Finally, close the runtime and return true if there were no errors logged.

// close the runspace
runspace
.Close();

return !Log.HasLoggedErrors;

That's it for the MSBuild task. Now, to use the task, it must either be installed to the GAC, or placed in a known location. Once that is established, other projects can be configured to consume this new task.

Consuming the MSBuild Task

Consider, for example, a C#-based class library project (.csproj). Integrating the task in a post build event requires just a few things.

First, register the task just inside the <Project> node of the .csproj file like so:

<UsingTask TaskName="PsBuildTask"
           
AssemblyFile="..\Noc.PsBuild\bin\Debug\Noc.PsBuild.dll" />

TaskName should be the name of the task, though it appears namespace is not required. Assembly file is an absolute path to the custom MSBuild task assembly, or relative path with respect to the .csproj file. For assemblies in the GAC, you can use the AssemblyName attribute instead.

Once registered, the task can be used within pre- and post-build events. Configure a build event within the <Project> element of the .csproj file like so:

<Target Name="AfterBuild">
   
<PsBuildTask ScriptPath=".\script.ps1" />
</Target>

And that's it. When Visual Studio compiles the project, it loads the custom assembly and task object and executes the task. Errors raised by the pipeline are retrieved and reported.

Now, a final note about the sample project. Visual Studio has a nasty habit of locking the Noc.PsBuild assembly each time Noc.PsBuild.Demo builds. That means that to rebuild this solution more than once requires restarting Visual Studio. This is all well and good for a demo, but of course this project structure, where the build task is in the same solution as a dependent assembly, is not ideal for real development. Also, the solution has only the "Works on My Machine" seal of approval. There could be gotchas, and it certainly could be extended to be even more flexible. Also, the script.ps1 file may be blocked if your execution policy is RemoteSigned. You'll need to unblock it or relax the policy to get successful execution. Good luck with it.

Happy coding!


 
Categories: MSBuild | PowerShell

Sample code: Noc.Demo.Clipboard.zip (163.36 KB)

I came across an interesting problem, the other day. The task at had was to copy an image to the Clipboard and maintain the alpha channel for pasting into applications such as Word, PowerPoint, Gimp, or Paint.NET.

What boggles my mind is: this is actually somewhat... well... hard.

Clipboard handling that the .NET Framework forgot

Don't get me wrong. Getting an image onto the clipboard is somewhat easy. It's a one-liner, really:

Clipboard.SetImage(myImage);

The problem with this approach is that the image loses its alpha channel, converting an image like this:

... to an image like this:

 

The resulting image is a 24-bit per pixel bitmap, with the transparent background filled by a soft gray. I have it on good report that older versions of windows swap out the transparent color for a virulent blue.

Now, I've seen applications cop out of transparency by using black or white, or even a gut wrenching magenta, for the intent of masking out the default color at run time. But gray? Why not try to be right some of the time instead of being wrong all of the time. Who ever really wants to swap out transparency for soft gray?

Covering up the gray with a little color

We all know someone who does it. Age creeps up and it's off to the salon for some dye.

When transparency is not strictly a requirement, a little bit of code can get us on the way to filling an image with a solid background. For this, I prefer the following extension method:

public static Image CreateOpaqueBitmap(this Image image, Color backgroundColor)
{
   
var bitmap = new Bitmap(image.Width, image.Height, PixelFormat.Format24bppRgb);
   
using (var graphics = Graphics.FromImage(bitmap))
   
{
        graphics
.Clear(backgroundColor);
        graphics
.DrawImage(image, 0, 0, image.Width, image.Height);
   
}

   
return bitmap;
}

With the extension method in hand, adding an opaque image to the clipboard was never easier:

using (var bitmap = image.CreateOpaqueBitmap(Color.Magenta))
{
   
Clipboard.Clear();
   
Clipboard.SetImage(bitmap);
}

But I really need transparency!

Creating opaque images really only massages an ache, but really doesn't solve the problem. What can be done about transparency?

A complete answer requires a little understanding of Windows clipboard formats. It helps to think of clipboard data as a set of key-value pairs. The key identifies what format the data is expected to be, and the value is the actual data. The clipboard can have multiple kinds of data, all representing (supposedly) the same thing, at any given time.

Some applications can take advantage of this fact to provide text data along with an image, so that pastes will provide something if pasting in a text editor, and something else if pasted in an image editor.

What is more often the case is that an application will provide multiple image formats to increase compatibility with other image software. As a matter of fact, Windows provides free type conversions between some of the most common image types (e.g. Bitmap to System.Drawing.Bitmap).

It helps to get a feel for what formats are supported for pasting in an application and their relative priority by inspecting what formats are generated when copying from the application. Here are formats generated when copying images from a few favorite applications:

  • GIMP: PNG, DeviceIndependentBitmap, System.Drawing.Bitmap, Bitmap, Format17
  • Paint.NET: System.Drawing.Bitmap, Bitmap, PaintDotNet.MaskedSurface
  • Fireworks: Fireworks Internal Clipboard Format 3.0, PNG, DeviceIndependentBitmap
  • MS Paint: Embed Source, Object Descriptor, MetaFilePict, DeviceIndependentBitmap
  • Print Screen (on the keyboard): System.Drawing.Bitmap, Bitmap, DeviceIndependentBitmap, Format17
  • PhotoScape: System.Drawing.Bitmap, Bitmap, DeviceIndependentBitmap, Format17
  • MS Word: Art::GVML ClipFormat, System.Drawing.Bitmap, Bitmap, PNG, JFIF, GIF, EnhancedMetafile, MetaFilePict, Object Descriptor
  • MS PowerPoint: Preferred DropEffect, InShellDragLoop, PowerPoint 12.0 Internal Shapes, Object Descriptor, Art::GVML ClipFormat, PNG, JFIF, GIF, System.Drawing.Bitmap, Bitmap, EnhancedMetafile, MetaFilePict, PowerPoint 12.0 Internal Theme, PowerPoint 12.0 Internal Color Scheme

Adding transparency with PNG.

It would seem that DeviceIndependentBitmap, Bitmap, and System.Drawing.Bitmap are the most common formats in the group; however, they are 24-bit opaque bitmaps. Generating a transparent clipboard data type requires that we properly encode data using a format that supports an alpha channel.

In the applications above, the PNG (Portable Network Graphics) format is generated by GIMP, Fireworks, Word, and PowerPoint. We can hope for transparency success in a broad array of applications merely by placing PNG-formatted data on the clipboard. Lucky for us, it doesn't take much to write:

using (var stream = new MemoryStream())
{
    image
.Save(stream, ImageFormat.Png);
   
var data = new DataObject("PNG", stream);

   
Clipboard.Clear();
   
Clipboard.SetDataObject(data, true);
}

Adding transparency with 32-bit ARGB bitmap

Not every application supports PNG formats. It is probably a good idea to have a secondary format at hand to deal with transparency. It is possible to get a 32bpp image into the clipboard with a little bit of work.

The format at hand, Format17 (a.k.a. CF_DIBV5)

I almost hate to bring it up, really, mostly because so many applications either mishandle it, or ignore it altogether. Take the print screen utility, for example. It does generate valid 32bpp Format17 content, but zeroes all the alpha bytes, and keeps a zero-ing byte mask for alpha just in case. In other words, it may be using a transparency-enabled format, but the content isn't transparent. GIMP mishandles the BITMAPV5HEADER, by accidentally omitting the bV5SizeImage field that is responsible for identifying how many bits are in the bitmap. (In fairness to GIMP, a clever application could compute the value based on other fields in the header.) Paint.NET appears to disregard formats other than 24bpp bitmaps and its own internal MaskedSurface format. Both PhotoScape and GIMP either disregard Format17 altogether (in favor of Bitmap formats) or mishandle the alpha, giving paste results like this:

So, with "You probably won't find this useful," as a disclaimer, let's see how it's done.

The first method is a utility method that copies a 32bpp image into global memory using interop and a little native marshalling. Basically, the method creates a bitmap header and marshals it out into memory, then adds the image bits, last row first, after the header. Note that all the interop methods and constants are omitted for brevity.

private static IntPtr CreatePackedDIBV5(this Bitmap bitmap)
{
   
BitmapData bmData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat);
   
uint bufferLen = (uint)(Marshal.SizeOf(typeof(BITMAPV5HEADER)) + bmData.Height * bmData.Stride);
   
IntPtr hMem = Kernel32.NativeMethods.GlobalAlloc(User32.GHND | User32.GMEM_DDESHARE, bufferLen);
   
IntPtr packedDIBV5 = Kernel32.NativeMethods.GlobalLock(hMem);

    BITMAPV5HEADER bmi
= (BITMAPV5HEADER)Marshal.PtrToStructure(packedDIBV5, typeof(BITMAPV5HEADER));
    bmi
.bV5Size = (uint)Marshal.SizeOf(typeof(BITMAPV5HEADER));
    bmi
.bV5Width = bmData.Width;
    bmi
.bV5Height = bmData.Height;
    bmi
.bV5Planes = 1;
    bmi
.bV5BitCount = 32;
    bmi
.bV5Compression = User32.BI_BITFIELDS;
    bmi
.bV5SizeImage = (uint)(bmData.Height * bmData.Stride);
    bmi
.bV5XPelsPerMeter = 0;
    bmi
.bV5YPelsPerMeter = 0;
    bmi
.bV5ClrUsed = 0;
    bmi
.bV5ClrImportant = 0;
    bmi
.bV5RedMask = 0x00FF0000;
    bmi
.bV5GreenMask = 0x0000FF00;
    bmi
.bV5BlueMask = 0x000000FF;
    bmi
.bV5AlphaMask = 0xFF000000;
    bmi
.bV5CSType = 0x73524742; // User32.LCS_WINDOWS_COLOR_SPACE;
    bmi
.bV5Endpoints.ciexyzBlue.ciexyzX = 0;
    bmi
.bV5Endpoints.ciexyzBlue.ciexyzY = 0;
    bmi
.bV5Endpoints.ciexyzBlue.ciexyzZ = 0;
    bmi
.bV5Endpoints.ciexyzGreen.ciexyzX = 0;
    bmi
.bV5Endpoints.ciexyzGreen.ciexyzY = 0;
    bmi
.bV5Endpoints.ciexyzGreen.ciexyzZ = 0;
    bmi
.bV5Endpoints.ciexyzRed.ciexyzX = 0;
    bmi
.bV5Endpoints.ciexyzRed.ciexyzY = 0;
    bmi
.bV5Endpoints.ciexyzRed.ciexyzZ = 0;
    bmi
.bV5GammaRed = 0;
    bmi
.bV5GammaGreen = 0;
    bmi
.bV5GammaBlue = 0;
    bmi
.bV5ProfileData = 0;
    bmi
.bV5ProfileSize = 0;
    bmi
.bV5Reserved = 0;
    bmi
.bV5Intent = User32.LCS_GM_IMAGES;
   
Marshal.StructureToPtr(bmi, packedDIBV5, false);

   
long offsetBits = bmi.bV5Size;
   
IntPtr bits = (IntPtr)(packedDIBV5.ToInt32() + offsetBits);
   
for (int y = 0; y < bmData.Height; y++)
   
{
       
IntPtr DstDib = (IntPtr)(bits.ToInt32() + (y * bmData.Stride));
       
IntPtr SrcDib = (IntPtr)(bmData.Scan0.ToInt32() + ((bmData.Height - 1 - y) * bmData.Stride));
       
for (int x = 0; x < bmData.Width; x++)
       
{
           
Marshal.WriteInt32(DstDib, Marshal.ReadInt32(SrcDib));
           
DstDib = (IntPtr)(DstDib.ToInt32() + 4);
           
SrcDib = (IntPtr)(SrcDib.ToInt32() + 4);
       
}
   
}

    bitmap
.UnlockBits(bmData);
   
Kernel32.NativeMethods.GlobalUnlock(hMem);
   
return hMem;
}

With the utility method at hand, adding an image to the clipboard using CF_DIBV5 (Format17) formatting is as easy as:

public static void Copy32BppBitmapToClipboard(this Image image)
{
   
using (var bitmap = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb))
   
{
       
using (var bitmapGraphics = Graphics.FromImage(bitmap))
       
{
            bitmapGraphics
.DrawImage(image, 0, 0, image.Width, image.Height);
       
}

       
var packedDIBV5 = CreatePackedDIBV5(bitmap);
       
User32.NativeMethods.OpenClipboard(IntPtr.Zero);
       
User32.NativeMethods.EmptyClipboard();
       
User32.NativeMethods.SetClipboardData(User32.CF_DIBV5, packedDIBV5);
       
User32.NativeMethods.CloseClipboard();
   
}
}

Bringing it home

I consider the 32bpp CF_DIBV5 format to be a lot of work, considering the poor support of the format offered by so many applications. I've found that taking a pragmatic approach, aiming for PNG transparency where supported, and accepting the background color of my choice otherwise, made for a manageable codebase with decent application compatibility. I use the following extension method to add images to the clipboard:

public static void CopyMultiFormatBitmapToClipboard(this Image image)
{
   
using (var opaque = image.CreateOpaqueBitmap(Color.White))
   
using (var stream = new MemoryStream())
   
{
        image
.Save(stream, ImageFormat.Png);

       
Clipboard.Clear();
       
var data = new DataObject();
        data
.SetData(DataFormats.Bitmap, true, opaque);
        data
.SetData("PNG", true, stream);
       
Clipboard.SetDataObject(data, true);
   
}
}

The results are fairly decent. Here are a few of the applications I tried:

  • GIMP: transparency supported
  • Fireworks: transparency supported
  • PhotoScape: white background
  • Paint.NET: white background
  • MS PowerPoint: transparency supported
  • MS Word: white background
  • MS Paint: white background

Happy coding!


Noc.Demo.Clipboard.zip (163.36 KB)


 
Categories: C# | Extension Methods