If you're unfamiliar with multithreading, be sure to check out my previous entries on the topic.
In WPF, creating a thread is as easy as it is with C#. You can find an example on that here. Alternatively, you could use the BackgroundWorker, which basically will create a thread and will give you a generalized, simplified interface in which to interact with it for a common threading task: doing an extra task in the background (such as downloading or progress bar updating).
In an earlier post, I used a mysterious method to enable responsiveness in the UI while loading a bunch of content (in that case, images).
This mystical object is called The Dispatcher.

No, this isn't an edge-of-your-seat thrill ride movie that smacks explosions, swords, and alien guts into your M&M-filled mouth. It is an object used to manage the work for threads within WPF. It maintains a queue of work items that are requested of any given thread, based on their order and priority. This is the object you want to get to know if you're going to be playing with your UI on a separate thread.
As mentioned previously, UI objects can't be accessed outside of the threads that created them. You can, however, use a separate thread to determine what changes you'll be making and to what objects you will make them, then use their thread to actually apply that change. In order to do this, use the object's dispatcher to schedule the work on their queue.
For example, take a look at the code for loading images mentioned above:
private void LoadImage(string fname)
{
// instantiate and initialize the image source
BitmapImage bmi = new BitmapImage();
bmi.BeginInit();
bmi.UriSource = new Uri(fname, UriKind.Relative);
bmi.EndInit();
bmi.Freeze(); // freeze the image source, used to move it across the thread
// this method tells the separate thread to run the following method to run on the UI thread
// the (ThreadStart)delegate(){ } notation is a shorthand for creating a method and a delegate for that method
TheImage.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (ThreadStart)delegate ()
{
TheImage.Source = bmi;
});
}
This method creates the BitmampImage object on a separate thread, leaving the main thread free for user input, and freezes it so that it can be used on another thread. It then uses TheImage's Dispatcher to modify TheImage on its own thread by calling its dispatcher's BeginInvoke method.
There are two ways to invoke using the dispatcher: BeginInvoke() and Invoke(). BeginInvoke() will queue the work for the dispatcher and continue the separate thread's execution. It puts in the request for the UI thread to execute the delegate, then continues on it merry way for its own execution. This is useful when your separate thread does not rely on what it is requesting the UI thread to do.
The Invoke() method will wait until the delegate is executed and returns. If you are modifying anything that you need or if the modification must be done before continuing in the separate thread, you should go with this one.
The Dispatcher is something you'll get pretty cozy with if you plan on changing your UI elements from a separate thread. If you're just doing a progress bar or something else that is rather predictable, you can skip it by using the BackgroundWorker's ReportProgress method and ProgressChanged event. Just be sure to give it some time if you are calling the dispatcher often.
In case you didn't notice all of my linking to previous posts, you may want to check out the rest of my posts on multithreading.
Tagging is starting to infiltrate all of our media. It makes organizing and using it easier, and is a valuable feature to support.
Reading metadata from an image is fairly simple, once you find the pieces. Vista uses the property Keywords to store tags, so we'll use that as well. The BitmapMetadata object holds quite a few properties for common metadata, such as Author, Rating, and Date Taken.
public string[] GetTags(string filename)
{
// open a filestream for the file we wish to look at
using (Stream fs = File.Open(filename, FileMode.Open, FileAccess.ReadWrite))
{
// create a decoder to parse the file
BitmapDecoder decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.None, BitmapCacheOption.Default);
// grab the bitmap frame, which contains the metadata
BitmapFrame frame = decoder.Frames[0];
// get the metadata as BitmapMetadata
BitmapMetadata metadata = frame.Metadata as BitmapMetadata;
// close the stream before returning
fs.Close();
// return a null array if keywords don't exist. otherwise, return a string array
if (metadata != null && metadata.Keywords != null)
return metadata.Keywords.ToArray();
else
return null;
}
}
If there is another metadata property you'd like to access, use the GetQuery method instead:
public string[] GetTags(string filename)
{
// open a filestream for the file we wish to look at
using (Stream fs = File.Open(filename, FileMode.Open, FileAccess.ReadWrite))
{
// create a decoder to parse the file
BitmapDecoder decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.None, BitmapCacheOption.Default);
// grab the bitmap frame, which contains the metadata
BitmapFrame frame = decoder.Frames[0];
// get the metadata as BitmapMetadata
BitmapMetadata metadata = frame.Metadata as BitmapMetadata;
// close the stream before returning
fs.Close();
// System.Keywords is the same as using the above method. this particular metadata property
// will return an array of strings, though we still have to cast it as such
string[] tags = metadata.GetQuery("System.Keywords") as string[];
return tags;
}
}
There is plenty that can go wrong here - the file could not exist, it could be in use, or not be an image at all. Be sure to handle the various exceptions accordingly.
Writing tags can actually get a little hairy, because space for metadata isn't necessarily alotted to every file. In actuality, it isn't there unless an application (which, notably, will be yours after you implement this) makes some room for it. When writing tags, you still need to get to the associated BitmapMetadata object. Where the method departs from reading is instantiating an InPlaceBitmapMetadataWriter object. This is what we use to write the data (assuming there is space for it). If that fails, then we address the issue of adding space for the metadata.
public static void AddTags(string filename, string[] tags)
{
// open a filestream for the file we wish to look at
using (Stream fs = File.Open(filename, FileMode.Open, FileAccess.ReadWrite))
{
// create a decoder to parse the file
BitmapDecoder decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.None, BitmapCacheOption.Default);
// grab the bitmap frame, which contains the metadata
BitmapFrame frame = decoder.Frames[0];
// get the metadata as BitmapMetadata
BitmapMetadata metadata = frame.Metadata as BitmapMetadata;
// instantiate InPlaceBitmapMetadataWriter to write the metadata to the file
InPlaceBitmapMetadataWriter writer = frame.CreateInPlaceBitmapMetadataWriter();
string[] keys;
if (metadata.Keywords != null) // tags exist - include them when saving
{
// build the complete list of tags - new and old
keys = new string[metadata.Keywords.Count + tags.Length];
int i = 0;
foreach (string keyword in metadata.Keywords)
{
keys[i] = keyword;
i++;
}
foreach (string tag in tags)
{
keys[i] = tag;
i++;
}
// associate the tags with the writer
// the type of variable to pass (here, an array of strings) depends on
// which metadata property you are using. Since we are modifying the
// Keywords property, we use the array. If you use the author property,
// it will simply be a string.
writer.SetQuery("System.Keywords", keys);
}
else // no old tags - just use the new ones
{
keys = tags;
// associate the tags with the writer
// the type of variable to pass (here, an array of strings) depends on
// which metadata property you are using. Since we are modifying the
// Keywords property, we use the array. If you use the author property,
// it will simply be a string.
writer.SetQuery("System.Keywords", tags);
}
// try to save the metadata to the file
if (!writer.TrySave())
{
// if it fails, there is no room for the metadata to be written to.
// we must add room to the file using SetUpMetadataOnImage (defined below)
SetUpMetadataOnImage(filename, keys);
}
}
}
So, if we have space allotted for metadata in the file, this will work great. Again, be sure to add exception handling.
What if we need to add room for the metadata? Well, this requires creating a new file, which will overwrite the old one. Essentially, we open it up, copy the image data from the old file, add whatever metadata we want to add initially, and save the file. This is a lossless transcoding, so no quality is lost. This does mean that the file size grows, however.
private static void SetUpMetadataOnImage(string filename, string[] tags)
{
// padding amount, using 2Kb. don't need much here; metadata is rather small
uint paddingAmount = 2048;
// open image file to read
using (Stream file = File.Open(filename, FileMode.Open, FileAccess.Read))
{
// create the decoder for the original file. The BitmapCreateOptions and BitmapCacheOption denote
// a lossless transocde. We want to preserve the pixels and cache it on load. Otherwise, we will lose
// quality or even not have the file ready when we save, resulting in 0b of data written
BitmapDecoder original = BitmapDecoder.Create(file, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
// create an encoder for the output file
JpegBitmapEncoder output = new JpegBitmapEncoder();
// add padding and tags to the file, as well as clone the data to another object
if (original.Frames[0] != null && original.Frames[0].Metadata != null)
{
// Because the file is in use, the BitmapMetadata object is frozen.
// So, we clone the object and add in the padding.
BitmapFrame frameCopy = (BitmapFrame)original.Frames[0].Clone();
BitmapMetadata metadata = original.Frames[0].Metadata.Clone() as BitmapMetadata;
// we use the same method described in AddTags() as saving tags to save an amount of padding
metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", paddingAmount);
metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", paddingAmount);
metadata.SetQuery("/xmp/PaddingSchema:Padding", paddingAmount);
// we add the tags we want as well. Again, using the same method described above
metadata.SetQuery("System.Keywords", tags);
// finally, we create a new frame that has all of this new metadata, along with the data that was in the original message
output.Frames.Add(BitmapFrame.Create(frameCopy, frameCopy.Thumbnail, metadata, frameCopy.ColorContexts));original.Frames[0].ColorContexts));
file.Close(); // close the file to ready for overwrite
}
// finally, save the new file over the old file
using (Stream outputFile = File.Open(filename, FileMode.Create, FileAccess.Write))
{
output.Save(outputFile);
}
}
}
There it is. Not entirely straight-forward, but it works nonetheless.
Updated: thanks to Thomas Manz and Frank Paris for bringing a couple flaws to my attention (see the comments).