views:

1025

answers:

1

I'm trying to write code that will load an image from a resource, and then crop it. This code works when I do all, or part, of it in XAML. I want to switch from all-XAML to all-code, so I can reuse this more than one place, with different Uris.

But when I try to do the same thing in code, I get a DirectoryNotFoundException, because suddenly it starts trying to look for a folder on disk, instead of loading the image from the resource.

  • If I load the BitmapImage in XAML, and then create a CroppedBitmap in XAML, everything works.
  • If I load the BitmapImage in XAML, and then write code to create a CroppedBitmap from it, everything works.
  • If I load the BitmapImage in code, without creating a CroppedBitmap from it, everything works.
  • But if I load the BitmapImage in code and create a CroppedBitmap in code, it tries to load from the filesystem instead of the resources, and I get a DirectoryNotFoundException.

Code samples are below. I'm sure I'm doing something stupid, but I've run through the whole thing three times now (once in my real app, once in a test app, and once while writing up this question), and I got the same results all three times.

For all of the following code samples, I've created an Images folder inside my project, and added an existing image there called "elf.png", with properties set to defaults (Build Action = "Resource"; Copy to Output Directory = "Do not copy").


Case 1: Both BitmapImage and CroppedBitmap in XAML.

<Window x:Class="WpfApplication8.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <BitmapImage x:Key="fullImage" UriSource="Images/elf.png"/>
        <CroppedBitmap x:Key="croppedImage" Source="{StaticResource fullImage}"
                       SourceRect="0 0 240 320"/>
    </Window.Resources>
    <Image Source="{StaticResource croppedImage}"/>
</Window>

This shows the cropped portion of the bitmap, as expected.


Case 2: BitmapImage in XAML; CroppedBitmap in code-behind.

XAML:

<Window x:Class="WpfApplication8.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <BitmapImage x:Key="fullImage" UriSource="Images/elf.png"/>
    </Window.Resources>
    <Image Name="image"/>
</Window>

Constructor in code-behind:

public Window1()
{
    InitializeComponent();
    var fullImage = (BitmapImage) FindResource("fullImage");
    var croppedImage =
        new CroppedBitmap(fullImage, new Int32Rect(0, 0, 240, 320));
    image.Source = croppedImage;
}

This also shows the cropped portion of the bitmap, as expected.


Case 3: BitmapImage in code; no CroppedBitmap.

public Window1()
{
    InitializeComponent();
    var uri = new Uri("Images/elf.png", UriKind.RelativeOrAbsolute);
    var fullImage = new BitmapImage(uri);
    image.Source = fullImage;
}

This shows the entire bitmap. This isn't what I want, but does tell me that I know how to write C# code to create the right kind of Uri and load a BitmapImage from a resource.


Case 4: BitmapImage and CroppedBitmap in code.

    public Window1()
    {
        InitializeComponent();
        var uri = new Uri("Images/elf.png", UriKind.RelativeOrAbsolute);
        var fullImage = new BitmapImage(uri);
        var croppedImage =
            new CroppedBitmap(fullImage, new Int32Rect(0, 0, 240, 320));
        image.Source = croppedImage;
    }

As far as I can tell, this just puts together the same pieces as before. It uses code that I know will load a BitmapImage from a resource, and code that I know will crop a section from a loaded BitmapImage. But somehow, when the two are put together, it forgets that the resource is there, and tries to load from disk. I get the following exception:

  • XamlParseException: "Cannot create instance of 'Window1' defined in assembly 'WpfApplication8, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Exception has been thrown by the target of an invocation. Error in markup file 'Window1.xaml' Line 1 Position 13."
    • Inner exception: TargetInvocationException: "Exception has been thrown by the target of an invocation."
      • Inner exception: DirectoryNotFoundException: "Could not find a part of the path 'C:\svn\WpfApplication8\WpfApplication8\bin\Debug\Images\elf.png'."

The inner-inner-exception stack trace shows that the original exception (the DirectoryNotFoundException) is being thrown by the line that instantiates the CroppedBitmap. I don't know why that line would be trying to read from disk, or why it doesn't work when the as-far-as-I-can-tell-equivalent XAML works fine.

Since I know the XAML is using the parameterless constructors, I also tried the following version, which should be much closer to what the XAML actually does:

public Window1()
{
    InitializeComponent();
    var uri = new Uri("Images/elf.png", UriKind.RelativeOrAbsolute);
    var fullImage = new BitmapImage();
    fullImage.BeginInit();
    fullImage.UriSource = uri;
    fullImage.EndInit();
    var croppedImage = new CroppedBitmap();
    croppedImage.BeginInit();
    croppedImage.Source = fullImage;
    croppedImage.SourceRect = new Int32Rect(0, 0, 240, 320);
    croppedImage.EndInit();
    image.Source = croppedImage;
}

Same exception, this time from the croppedImage.EndInit(); line.

Any ideas on how I can get the all-code version to correctly load the resource and crop the image? What's happening in the XAML version that's different?

+1  A: 

The magic turned out to be in the BitmapImage's BaseUri property. BaseUri apparently serves as the "current directory" that UriSource is relative to.

When my BitmapImage was being loaded from XAML, BaseUri was being magically set to "pack://application:,,,/WpfApplication8;component/window1.xaml". When I modified code snippet #4 to explicitly set fullImage.BaseUri to that same value before creating the CroppedBitmap, everything worked.

Why it worked from XAML (and from just-BitmapImage-without-CroppedBitmap)

So where did this magic BaseUri value come from?

BaseUri is part of the IUriContext interface. IUriContext.BaseUri is set in two places in the WPF assemblies, and between my various examples, I managed to hit both of them. No wonder I was confused.

  • BamlRecordReader.ElementInitialize. The BAML loader automatically sets BaseUri anytime it loads an element that implements IUriContext. This explains why my examples #1 and #2 worked: they were loading from the compiled BAML resource.
  • Image.UpdateBaseUri (called whenever the Source property is changed). This checks to see if the Source implements IUriContext, and if so, sets its BaseUri. This explains why my example #3 worked: pushing the BitmapImage into the GUI forced it to get the right search path.

It only looks for the image in the EXE resources when BaseUri is set to the magic pack:// URI. Without that (as happens when everything is created in code and not pushed into the GUI), it only looks on disk.

The fix

As noted above, I could hard-code BaseUri. But the BaseUriHelper class provides a better fix:

fullImage.BaseUri = BaseUriHelper.GetBaseUri(this);

This sets fullImage to have the same BaseUri as the window (this). If this is done before creating the CroppedBitmap, everything works.

Joe White