Figure 1: WPF-MVVM Demo App
Figure 2: Windows Forms Demo App
Introduction
I needed an Exif reader class for a C# application I was working on, and
though I found quite a few implementations available including a few on The Code
Project, none of them fully suited my requirements. So I wrote my own class.
This article describes the use and implementation of this class, and also
includes a couple of demo projects, one using Windows Forms and the
PropertyGrid
, and another using WPF and the growingly popular MVVM
architecture. The ExifReader project is 100% Style Cop compliant except
for the IDE generated AssemblyInfo.cs and the PropertyTagId.cs
file which I custom created from multiple sources including some GDI+ header
files, as well as trial and error. I didn't think it would be a great idea to
try and apply Style Cop guidelines to either of those files. The class does not
directly access Exif metadata from image files, and instead delegates that
functionality to the Image
class from System.Drawing. The
public interface does not expose any System.Drawing types and so this
library can be used from WPF without needing to reference System.Drawing.
Note to readers/users of this class
This class tries to cover as many of the documented and undocumented Exif
tags as I could test on, but if any of you encounter images that have Exif tags
that are not recognized by my class, I request you to kindly contact me and send
me the images. Once I have the images, I can attempt to support those tags too.
Right now, if it encounters an undocumented tag it will still extract the tag
value, but you won't see any descriptive tag-name or any custom
parsing/formatting that may be required.
Code was written on VS 2010 and .NET 4.0
All the code, including the demos, have been written and tested on VS 2010 RC
and .NET 4.0. While I have not intentionally used any .NET 4.0/C# 4.0 only
feature like say the dynamic
keyword, I may have inadvertently
written code that may not compile in .NET 3.5. But I don't expect any of those
to be hard to fix or change for most programmers, but if you run into trouble
and can't figure out what to do, please ping me via the article forum and I'll
help fix it. I suspect the most common compatibility issues would be with regard
to IEnumerable<T>
cast requirements in .NET 3.5, since it became a variant
interface
only in .NET 4.0.
Inline code formatting
The inline code snippets in this article have been wrapped for easier
viewing. The actual code in the source files don't assume that you are using a
10 inch Netbook. *grin*
Using the ExifReader class
If you are using an Exif reader class, I guess it's safe to assume that you
are familiar with Exif properties and the Exif specification. So I won't go into
the details. For those who are interested, here are two good links:
The second link includes the Exif 2.2 specification. The 2.21 specification
is not available for free download, though there are sites where you can
purchase it. You will also find that many cameras and phones insert Exif 2.21
tags even though the Exif version tag is still set to 2.2, and you will also
find that applications like Adobe Photoshop insert their own custom tags. Camera
manufacturers like Nikon and Canon have their own proprietary tags too. I have
basically stuck to the standard tags and have not attempted to decipher any of
the company proprietary tags, though it's possible that a future version may
support those too.
The class interface is fairly trivial, all you do is instantiate an
ExifReader
object passing a file path, and then access the extracted Exif
properties with a call to GetExifProperties
. Here's some example code:
internal class Program
{
internal static void Main(string[] args)
{
ExifReader exifReader = new ExifReader(
@"C:\Users\Public\Pictures\Sample Pictures\Jellyfish.jpg");
foreach (ExifProperty item in exifReader.GetExifProperties())
{
Console.WriteLine("{0} = {1}", item.ExifPropertyName, item);
}
Console.ReadKey();
}
}
The ExifProperty
type will be described in a little more detail
in the next two sections where I talk about the two demo applications.
Essentially it's the only other type you'll have to deal with when using the
ExifReader
class. You can further dig into the property data by
using ExifProperty.ExifValue
if you need to as shown below, but
it's an unlikely scenario unless you are dealing with a custom Exif tag type
unique to your application, or if it's an unhandled proprietary tag as those
from Nikon, Canon, or Adobe, in which case you may want to access the values
directly and do your own interpretation or formatting.
ExifProperty copyRight = exifReader.GetExifProperties().Where(
ep => ep.ExifTag == PropertyTagId.Copyright).FirstOrDefault();
if (copyRight != null)
{
Console.WriteLine(copyRight.ExifValue.Count);
Console.WriteLine(copyRight.ExifValue.ValueType);
foreach (var value in copyRight.ExifValue.Values)
{
Console.WriteLine(value);
}
}
PropertyTagId
is an enum
that represents the
documented Exif-Tag properties as well as a few undocumented ones. There is also
a PropertyTagType
enum
that represents the Exif data
type. Both these enumerations will be discussed in later sections of the
article.
Using custom formatters and extractors
The ExifReader
supports custom handling of tags, which is useful
not just for undocumented tags that are not automatically handled by the class,
but also for specialized handling of documented tags. There are two events
QueryUndefinedExtractor
and QueryPropertyFormatter
that you
need to handle, and these events allow you to specify a custom formatter object
as well as a custom extractor object. Note that it's usually convenient but not
necessary to implement both the formatter and the extractor in a single class.
Also be aware that if you provide a custom extractor for a documented tag, then
it's highly recommended that you also implement a custom formatter, specially if
you change the extracted data type of the property. Here's an example of a very
simple tag handler class.
class MyCustomExifHandler
: IExifPropertyFormatter, IExifValueUndefinedExtractor
{
public string DisplayName
{
get { return "My Display Name"; }
}
public string GetFormattedString(IExifValue exifValue)
{
return String.Concat("Formatted version of ",
exifValue.Values.Cast<string>().First());
}
public IExifValue GetExifValue(byte[] value, int length)
{
string bytesString = String.Join(" ",
value.Select(b => b.ToString("X2")));
return new ExifValue<string>(
new[] { String.Concat("MyValue = ", bytesString) });
}
}
Both the implemented interfaces will be discussed later in this article. Once
you have a custom handler class, you can handle the events and set the
appropriate values on the EventArgs
object.
ExifReader exifReader = new ExifReader(@". . .");
MyCustomExifHandler exifHandler = new MyCustomExifHandler();
exifReader.QueryUndefinedExtractor += (sender, e) =>
{
if (e.TagId == 40093)
{
e.UndefinedExtractor = exifHandler;
}
};
exifReader.QueryPropertyFormatter += (sender, e) =>
{
if (e.TagId == 40093)
{
e.PropertyFormatter = exifHandler;
}
};
foreach (ExifProperty item in exifReader.GetExifProperties())
{
. . .
}
Notice how it's of extreme importance to check the TagId
. If you
do not do this, you will be applying your custom formatter or extractor to every
single Exif tag that's extracted from the image and that's guaranteed to end in
disaster!
Demo app for Windows Forms
The Windows Forms demo uses a PropertyGrid
to show the various Exif
properties. The ExifReader
has
built-in support for the PropertyGrid
control, so
there is very little code you need to write. Here's the entire user-written code
in the Windows Forms demo project:
private void ButtonBrowse_Click(object sender, EventArgs e)
{
using (OpenFileDialog dialog = new OpenFileDialog()
{
Filter = "Image Files(*.PNG;*.JPG)|*.PNG;*.JPG;"
})
{
if (dialog.ShowDialog() == DialogResult.OK)
{
try
{
ExifReader exifReader = new ExifReader(dialog.FileName);
this.propertyGridExif.SelectedObject = exifReader;
this.pictureBoxPreview.ImageLocation = dialog.FileName;
}
catch (ExifReaderException ex)
{
MessageBox.Show(ex.Message, "ExifReaderException was caught");
}
}
}
}
The highlighted line shows where we associate the ExifReader
object with the PropertyGrid
control. Figure 2 shows a
screenshot of what the app would look like when run and an image is selected.
The reason this works this way is that the ExifReader
class has a
custom TypeDescriptionProvider
which dynamically provides the property
descriptors that the PropertyGrid
looks for. I've talked a little
about how this has been implemented down below in the Implementation Details
section. From a Windows Forms perspective, if you have a form where you need to
display the Exif properties, this would be the simplest way to go about doing
it. Drop a PropertyGrid
control on the form and then set the
SelectedObject
to an ExifReader
instance, and that's all
you need to do.
Handling exceptions
Notice how an exception of type ExifReaderException
is caught
and handled. The ExifReader
's constructor is the only place where
this exception is directly thrown. Exception handling is discussed in multiple
places in the article, but in brief, you typically get this exception here if a
file path is invalid or an image file is corrupt or has invalid Exif metadata.
Once the ExifReader
is instantiated, you will never get a direct
exception of type ExifReaderException
. The data binding (whether in
WinForms or WPF) will primarily be via property accessors and the ToString
override, and thus throwing arbitrary exception objects from these
locations is not recommended and quite against standard .NET practices. From all
these places, what's thrown is an exception of type
InvalidOperationException
which the majority of well-written data binding
libraries know how to handle correctly. You can still handle this exception in
the debugger and access the source ExifReaderException
object
via the InnerException
property of the
InvalidOperationException
object.
Demo app for WPF/MVVM
Three gentlemen by the names of Josh Smith, Sacha Barber, and Pete O'Hanlon
have been saying good things about MVVM here on The Code Project for at least a
couple of years now, and so I thought it would be a good idea to apply it for
the WPF demo app, even though the entire app is of a very simplistic nature. Of
course my implementation is rather plain and lacks the finesse and poise that we
now take for granted in articles and applications written by the afore mentioned
gentlemen. While it's not an absolute requisite that the View class is 100% Xaml
with zero code-behind, since this was my first MVVM app, I went out of my way to
make sure that there is no code-behind in use. While it's not connected with the
ExifReader
per se, I will still briefly run through what I
did to do what I wanted to do.
Figure 3: WPF-MVVM app with filter applied
The WPF demo does a little more than the WinForms demo. For one thing, it
shows the Exif data type and the Exif tag name for each extracted tag. And then
for another, it supports the ability to filter results using a search keyword
that searches the tag name as well as the tag property value. And then there's
also the fact that my demo app portrays my absolutely horrid sense of color
schemes, styles, and UI themes. I always give Sacha a hard time regarding his
rather unorthodox ideas on colors and styles, but I think I've surpassed even Sacha
in poor UI taste. *grin*
The ViewModel class
There's only one ViewModel class for this demo project, and it has
commands for browse, exit, and filter. There is also a public
CollectionViewSource
property called ExifProperties
which
returns the extracted Exif properties. Initially this was an
ObservableCollection<ExifProperty>
property but I had to change it to a
CollectionViewSource
property to adamantly stick with my decision
to avoid code-behind in the view class. Since I wanted to do filtering, this was
the simplest way I could use the built-in filtering support of the
CollectionViewSource
entirely inside the ViewModel class.
internal class MainViewModel : INotifyPropertyChanged
{
private ICommand browseCommand;
private ICommand exitCommand;
private ICommand filterCommand;
private string searchText = String.Empty;
private ObservableCollection<ExifProperty> exifPropertiesInternal
= new ObservableCollection<ExifProperty>();
private CollectionViewSource exifProperties = new CollectionViewSource();
private ImageSource previewImage;
public MainViewModel()
{
exifProperties.Source = exifPropertiesInternal;
exifProperties.Filter += ExifProperties_Filter;
}
public CollectionViewSource ExifProperties
{
get { return exifProperties; }
}
public ICommand BrowseCommand
{
get
{
return browseCommand ??
(browseCommand = new DelegateCommand(BrowseForImage));
}
}
public ICommand ExitCommand
{
get
{
return exitCommand ??
(exitCommand = new DelegateCommand(
() => Application.Current.Shutdown()));
}
}
public ICommand FilterCommand
{
get
{
return filterCommand ??
filterCommand = new DelegateCommand(
() => exifProperties.View.Refresh()));
}
}
public ImageSource PreviewImage
{
get
{
return previewImage;
}
set
{
if (previewImage != value)
{
previewImage = value;
FirePropertyChanged("PreviewImage");
}
}
}
public string SearchText
{
get
{
return searchText;
}
set
{
if (searchText != value)
{
searchText = value;
FirePropertyChanged("SearchText");
}
}
}
private void BrowseForImage()
{
}
private void ExifProperties_Filter(object sender, FilterEventArgs e)
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void FirePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Here's the code where the image is browsed to, and the ExifReader
instantiated. All the extracted properties are added to the
ObservableCollection<ExifProperty>
property, and the preview image
is appropriately set.
private void BrowseForImage()
{
OpenFileDialog fileDialog = new OpenFileDialog()
{
Filter = "Image Files(*.PNG;*.JPG)|*.PNG;*.JPG;"
};
if (fileDialog.ShowDialog().GetValueOrDefault())
{
try
{
ExifReader exifReader = new ExifReader(fileDialog.FileName);
exifPropertiesInternal.Clear();
this.SearchText = String.Empty;
foreach (var item in exifReader.GetExifProperties())
{
exifPropertiesInternal.Add(item);
}
this.PreviewImage = new BitmapImage(new Uri(fileDialog.FileName));
}
catch (ExifReaderException ex)
{
MessageBox.Show(ex.Message, "ExifReaderException was caught");
}
}
}
Note how similar to the WinForms demo, there's a try
-catch
handler specifically for an exception of type ExifReaderException
. For most apps, you may want to filter out specific tags at this point, or
maybe only show some pre-selected list of Exif properties. Either way there's
nothing there that a couple of lines of LINQ won't solve. I did consider adding
built-in support for filtering within the ExifReader
class but then
decided it deviates from the core class functionality, and does not really have
a lot of value anyway since the user can do that in one or two lines of code.
For the demo app, the filter is text-based and does a case insensitive search on
the Exif property name as well as the property value's string representation.
private void ExifProperties_Filter(object sender, FilterEventArgs e)
{
ExifProperty exifProperty = e.Item as ExifProperty;
if (exifProperty == null)
{
return;
}
foreach (string body in new[] {
exifProperty.ExifPropertyName,
exifProperty.ExifTag.ToString(), exifProperty.ToString() })
{
e.Accepted = body.IndexOf(
searchText, StringComparison.OrdinalIgnoreCase) != -1;
if (e.Accepted)
{
break;
}
}
}
The View
The view is the main Window
class in the project (I did say
this was a rather crude MVVM implementation, so don't smirk too much).
There's a ListBox
with a data template applied that displays the
Exif properties. Here's some partially snipped, and heavily word wrapped Xaml
code:
<ListBox Grid.Column="1" ItemsSource="{Binding ExifProperties.View}"
Background="Transparent" TextBlock.FontSize="11"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch">
. . .
<ListBox.ItemTemplate>
<DataTemplate>
<dropShadow:SystemDropShadowChrome CornerRadius="20,20,0,0">
<StackPanel Orientation="Vertical" Margin="5" Width="240"
Background="Transparent">
<Border CornerRadius="20,20,0,0" Background="#FF0D3C83"
x:Name="topPanel">
<StackPanel Orientation="Horizontal" Margin="6,0,0,0">
<TextBlock x:Name="titleText"
Text="{Binding ExifPropertyName}"
Foreground="White" FontWeight="Bold" />
</StackPanel>
</Border>
<StackPanel Orientation="Vertical"
Background="#FF338DBE" x:Name="mainPanel">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Exif Tag: "
FontWeight="Bold" MinWidth="100" />
<TextBlock Text="{Binding ExifTag}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Exif Datatype: "
FontWeight="Bold" MinWidth="100" />
<TextBlock Text="{Binding ExifDatatype}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Property Value: "
FontWeight="Bold" MinWidth="100" />
<TextBlock Text="{Binding}" />
</StackPanel>
</StackPanel>
</StackPanel>
</dropShadow:SystemDropShadowChrome>
. . .
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
I've removed the superfluous styling code. The binding points in code are
highlighted above, and the reason the property value binds to self is that the
ExifProperty
class has a ToString
override - which
makes it trouble-free to render readable text for the Exif property values. In
fact the Windows Forms PropertyGrid
uses ToString
too
and this ensures that you'll see consistent display values irrespective of how
you use the class.
I wanted the filtering to be live, as in the filter should take effect as the
user types into the text box. The big problem with this is that the
TextBox
does not have a Command
property, and thus I'd need
to use code-behind to proxy its text changed event to the ViewModel
. And I did not want
to do that. Once again this was purely whimsical behavior on my part since MVVM
does not mandate this at all, though a lot of people do recommend it. Apparently, you can
get around this by referencing an Expression Blend DLL, which has support for
interaction triggers that you can forward to a command object, thereby avoiding
any code-behind. I didn't want to reference an Expression Blend DLL, not for a
simple demo app anyway and so I was forced to work around it by adding an
attached behavior to the TextBox
that could take a command object.
This was total over-kill of course, and in a real world app I'd simply do it in
code-behind. Most likely something like :
ViewModel viewModel = this.DataContext as ViewModel;
. . .
private void TextChanged(. . .)
{
viewModel.SomeCommand(. . .) ;
}
That'd be in my view's code-behind, and while some purists may not be too
happy, I think it's definitely simpler than referencing Expression Blend! And to
paraphrase Rama Vavilala's words : "after all, these frameworks are supposed
to make things simpler". Here's the Xaml code snippet:
<TextBox x:Name="searchTextBox" Width="165"
HorizontalAlignment="Left" Margin="3,0,0,0"
Text="{Binding SearchText}"
local:TextChangedBehavior.TextChanged="{Binding FilterCommand}" />
Instead of handling the TextChanged
event, I handle it via the
attached behavior and route it to the FilterCommand
command object
in the ViewModel. Here's the code for the attached behavior:
internal class TextChangedBehavior
{
public static DependencyProperty TextChangedCommandProperty
= DependencyProperty.RegisterAttached(
"TextChanged",
typeof(ICommand),
typeof(TextChangedBehavior),
new FrameworkPropertyMetadata(
null,
new PropertyChangedCallback(
TextChangedBehavior.TextChangedChanged)));
public static void SetTextChanged(TextBox target, ICommand value)
{
target.SetValue(TextChangedBehavior.TextChangedCommandProperty,
value);
}
public static ICommand GetTextChanged(TextBox target)
{
return (ICommand)target.GetValue(TextChangedCommandProperty);
}
private static void TextChangedChanged(
DependencyObject target, DependencyPropertyChangedEventArgs e)
{
TextBox element = target as TextBox;
if (element != null)
{
if (e.NewValue != null)
{
element.TextChanged += Element_TextChanged;
}
else
{
element.TextChanged -= Element_TextChanged;
}
}
}
static void Element_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = (TextBox)sender;
BindingExpression bindingExpression = textBox.GetBindingExpression(
TextBox.TextProperty);
if (bindingExpression != null)
{
bindingExpression.UpdateSource();
}
ICommand command = GetTextChanged(textBox);
if (command.CanExecute(null))
{
command.Execute(null);
}
}
}
Nothing complicated there, just a basic attached behavior implementation. The
highlighted code shows where we proxy to the actual command implementation.
ExifReader Class Reference
As I already mentioned, except for that one enum
source file,
the rest of the ExifReader
code is fully StyleCop compliant, and
thus I had to add XML comments for every method, property, and field, including
private
ones. So the code is basically self documenting for the
most part. Once again the code snippets below are line wrapped for viewing, so
some of the comment lines will seem rather awkwardly split up into multiple
lines.
The ExifReader class
[TypeDescriptionProvider(typeof(ExifReaderTypeDescriptionProvider))]
public class ExifReader
{
private List<ExifProperty> exifproperties;
private Image imageFile;
public ExifReader(string imageFileName);
public event EventHandler<
QueryPropertyFormatterEventArgs> QueryPropertyFormatter;
public event EventHandler<
QueryUndefinedExtractorEventArgs> QueryUndefinedExtractor;
public ReadOnlyCollection<ExifProperty> GetExifProperties();
internal IExifPropertyFormatter QueryForCustomPropertyFormatter(int tagId);
internal IExifValueUndefinedExtractor
QueryForCustomUndefinedExtractor(int tagId);
private void FireQueryPropertyFormatter(
QueryPropertyFormatterEventArgs eventArgs);
private void FireQueryUndefinedExtractor(
QueryUndefinedExtractorEventArgs eventArgs);
private void InitializeExifProperties();
}
Earlier on, I had a constructor overload that took an
Image
argument but it was removed so that WPF projects can reference the DLL
without needing to reference the System.Drawing DLL.
The QueryPropertyFormatterEventArgs class
public class QueryPropertyFormatterEventArgs : EventArgs
{
public QueryPropertyFormatterEventArgs(int tagId);
public IExifPropertyFormatter PropertyFormatter { get; set; }
public int TagId { get; private set; }
}
The QueryUndefinedExtractorEventArgs class
public class QueryUndefinedExtractorEventArgs : EventArgs
{
public QueryUndefinedExtractorEventArgs(int tagId);
public IExifValueUndefinedExtractor UndefinedExtractor { get; set; }
public int TagId { get; private set; }
}
I considered unifying the above two EventArgs
classes into a
single generic
class
, but then decided to retain them
as separate classes since they are both used in public
event
handlers where I feel clarity is more important than compactness.
The ExifProperty class
public class ExifProperty
{
private PropertyItem propertyItem;
private IExifValue exifValue;
private IExifPropertyFormatter propertyFormatter;
private bool isUnknown;
private bool hasCustomFormatter;
private ExifReader parentReader;
internal ExifProperty(PropertyItem propertyItem, ExifReader parentReader);
public IExifValue ExifValue;
public string ExifPropertyName;
public string ExifPropertyCategory;
public PropertyTagId ExifTag;
public PropertyTagType ExifDatatype;
public int RawExifTagId;
public override string ToString();
private string GetFormattedString();
private IExifValue InitializeExifValue();
private ExifReaderException GetExifReaderException(Exception ex);
}
The IExifValue interface
public interface IExifValue
{
Type ValueType { get; }
int Count { get; }
IEnumerable Values { get; }
}
Normal usage would not require accessing the ExifProperty.ExifValue
property, but this is provided for custom interpretation of undocumented or
proprietary tags. Note how the IExifValue
interface actually
returns a System.Type
for the ValueType
property. I
designed it thus since the IExifValue
represents how the value is
represented internally by the ExifReader
class and not an actual
byte-based representation of the native Exif data. The user can still access
ExifProperty.ExifDatatype
to get the Exif data type associated with
the property.
The PropertyTagType enumeration
public enum PropertyTagType
{
Byte = 1,
ASCII = 2,
Short = 3,
Long = 4,
Rational = 5,
Undefined = 7,
SLong = 9,
SRational = 10
}
The PropertyTagId enumeration
public enum PropertyTagId
{
GpsVer = 0x0000,
GpsLatitudeRef = 0x0001,
. . .
<snipped>
This is too long to list here in its entirety, so please refer to the source
code. I do discuss a little about how I've used this enum
in the
whole Exif extraction process in the next section. I have added one special
value here to represent tags that I do not recognize, and I've chosen the
largest 16 bit value possible that I hope will not coincide with an actual Exif
tag value used by some proprietary Exif format.
[EnumDisplayName("Unknown Exif Tag")]
UnknownExifTag = 0xFFFF
I was originally casting the integer value to the enum
because
WinForms and its PropertyGrid
did not complain about invalid
enum
values, and ToString
would just return the numeric
string value which was all perfectly okay. Strictly speaking WPF did not
complain either, but I saw a first-chance exception in the output window and
traced it down to the fact that the default WPF data binding uses the
internal
class SourceDefaultValueConverter
from
PresentationFramework
which was throwing an ArgumentException
when it got an enum
value that was outside the enum
range. So I decided to go ahead with an UnknownExifTag
enum
value for values that were not in the enumeration. Oh, and I chose 0xFFFF
instead of 0
because GpsVer
has already used 0
!
You can still get the original Tag-Id by using the RawExifTagId
property which returns an int
.
The ExifReaderException class
[Serializable]
public class ExifReaderException : Exception
{
public ExifReaderException();
internal ExifReaderException(Exception innerException);
internal ExifReaderException(string message, Exception innerException);
internal ExifReaderException(Exception innerException,
IExifPropertyFormatter propertyFormatter);
internal ExifReaderException(Exception innerException,
IExifValueUndefinedExtractor undefinedExtractor);
internal ExifReaderException(Exception innerException,
IExifPropertyFormatter propertyFormatter,
IExifValueUndefinedExtractor undefinedExtractor);
internal ExifReaderException(string message,
Exception innerException,
IExifPropertyFormatter propertyFormatter,
IExifValueUndefinedExtractor undefinedExtractor);
public IExifPropertyFormatter PropertyFormatter { get; private set; }
public IExifValueUndefinedExtractor UndefinedExtractor
{ get; private set; }
public override void GetObjectData(
SerializationInfo info, StreamingContext context);
}
If you implement custom formatters or extractors, you should not throw an
ExifReaderException
, instead you should throw a standard exception
or even a custom exception specific to your application. The ExifReader
class will handle that exception and re-throw an ExifReaderException
which will have your thrown exception as the InnerException
,
and hence the internal constructors (barring the default constructor which is
left public for serialization).
Implementation Details
The source code is profusely commented and so anyone interested in getting a
broad understanding of the code should probably browse through the source code.
In this section I'll briefly talk about the basic design and also anything that
I think will be interesting to a percentage of readers. As mentioned in the
introduction, I use the System.Drawing Image
class to get
the Exif info out of a supported image file.
private void InitializeExifProperties()
{
this.exifproperties = new List<ExifProperty>();
foreach (var propertyItem in this.imageFile.PropertyItems)
{
this.exifproperties.Add(new ExifProperty(propertyItem, this));
}
}
The InitializeExifProperties
method
creates an ExifProperty
instance for every PropertyItem
returned by the Image
class. Before I go into what happens inside
ExifProperty
, I'd like to describe the IExifPropertyFormatter
interface that is used to get a readable display-value for an Exif property.
public interface IExifPropertyFormatter
{
string DisplayName { get; }
string GetFormattedString(IExifValue exifValue);
}
ExifProperty
has a field propertyFormatter
of type
IExifPropertyFormatter
which is initialized in the constructor.
internal ExifProperty(PropertyItem propertyItem, ExifReader parentReader)
{
this.parentReader = parentReader;
this.propertyItem = propertyItem;
this.isUnknown = !Enum.IsDefined(typeof(PropertyTagId),
this.RawExifTagId);
var customFormatter =
this.parentReader.QueryForCustomPropertyFormatter(this.RawExifTagId);
if (customFormatter == null)
{
this.propertyFormatter =
ExifPropertyFormatterProvider.GetExifPropertyFormatter(this.ExifTag);
}
else
{
this.propertyFormatter = customFormatter;
this.hasCustomFormatter = true;
}
}
The ExifProperty
constructor calls
QueryForCustomPropertyFormatter
on the parent ExifReader
class to see if there is an user specified property formatter available.
internal IExifPropertyFormatter QueryForCustomPropertyFormatter(
int tagId)
{
QueryPropertyFormatterEventArgs eventArgs =
new QueryPropertyFormatterEventArgs(tagId);
this.FireQueryPropertyFormatter(eventArgs);
return eventArgs.PropertyFormatter;
}
If there is no custom formatter provided, then a property formatter is
obtained via the ExifPropertyFormatterProvider
class.
Specifying property formatters via a custom enum attribute
ExifPropertyFormatterProvider
is a class that returns an
IExifPropertyFormatter
for a given Exif tag. It looks for a custom
attribute in the PropertyTagId
enumeration to figure out what
property formatter needs to be used. Here're a couple of examples of how an
enum
value might specify a property formatter.
[ExifPropertyFormatter(typeof(ExifExposureTimePropertyFormatter))]
ExifExposureTime = 0x829A,
[ExifPropertyFormatter(typeof(ExifFNumberPropertyFormatter),
ConstructorNeedsPropertyTag = true)]
[EnumDisplayName("F-Stop")]
ExifFNumber = 0x829D,
ExifPropertyFormatter
is a custom attribute that can be applied
to an enum
.
[AttributeUsage(AttributeTargets.Field)]
internal class ExifPropertyFormatterAttribute : Attribute
{
private IExifPropertyFormatter exifPropertyFormatter;
private Type exifPropertyFormatterType;
public ExifPropertyFormatterAttribute(Type exifPropertyFormatterType)
{
this.exifPropertyFormatterType = exifPropertyFormatterType;
}
public bool ConstructorNeedsPropertyTag { get; set; }
public IExifPropertyFormatter GetExifPropertyFormatter(
params object[] args)
{
return this.exifPropertyFormatter ??
(this.exifPropertyFormatter =
Activator.CreateInstance(
this.exifPropertyFormatterType,
args) as IExifPropertyFormatter);
}
}
And here's how the ExifPropertyFormatterProvider
class uses this
attribute to return the appropriate property formatter.
public static class ExifPropertyFormatterProvider
{
internal static IExifPropertyFormatter GetExifPropertyFormatter(
PropertyTagId tagId)
{
ExifPropertyFormatterAttribute attribute =
CachedAttributeExtractor<PropertyTagId,
ExifPropertyFormatterAttribute>.Instance.GetAttributeForField(
tagId.ToString());
if (attribute != null)
{
return attribute.ConstructorNeedsPropertyTag ?
attribute.GetExifPropertyFormatter(tagId) :
attribute.GetExifPropertyFormatter();
}
return new SimpleExifPropertyFormatter(tagId);
}
}
If the attribute is found, the attribute's associated property formatter is
returned. If there is no match, then a basic formatter is returned.
The CachedAttributeExtractor utility class
Notice how I've used the CachedAttributeExtractor
utility class
to extract the custom attribute. It's a handy little class that not only makes
it easy to extract attributes but also caches the attributes for later access.
internal class CachedAttributeExtractor<T, A> where A : Attribute
{
private static CachedAttributeExtractor<T, A> instance
= new CachedAttributeExtractor<T, A>();
private Dictionary<string, A> fieldAttributeMap
= new Dictionary<string, A>();
private CachedAttributeExtractor()
{
}
internal static CachedAttributeExtractor<T, A> Instance
{
get { return CachedAttributeExtractor<T, A>.instance; }
}
public A GetAttributeForField(string field)
{
A attribute;
if (!this.fieldAttributeMap.TryGetValue(field, out attribute))
{
if (this.TryExtractAttributeFromField(field, out attribute))
{
this.fieldAttributeMap[field] = attribute;
}
else
{
attribute = null;
}
}
return attribute;
}
private bool TryExtractAttributeFromField(
string field, out A attribute)
{
var fieldInfo = typeof(T).GetField(field);
attribute = null;
if (fieldInfo != null)
{
A[] attributes = fieldInfo.GetCustomAttributes(
typeof(A), false) as A[];
if (attributes.Length > 0)
{
attribute = attributes[0];
}
}
return attribute != null;
}
}
Creating the Exif values
Before going into how the various property formatters are implemented, I'll
take you through how the Exif values themselves are created. There's an
InitializeExifValue
method that's called to create the IExifValue
object for an ExifProperty
instance.
private IExifValue InitializeExifValue()
{
try
{
var customExtractor =
this.parentReader.QueryForCustomUndefinedExtractor(
this.RawExifTagId);
if (customExtractor != null)
{
return this.exifValue = customExtractor.GetExifValue(
this.propertyItem.Value, this.propertyItem.Len);
}
return this.exifValue =
this.ExifDatatype == PropertyTagType.Undefined ?
ExifValueCreator.CreateUndefined(
this.ExifTag,
this.propertyItem.Value,
this.propertyItem.Len) :
ExifValueCreator.Create(
this.ExifDatatype,
this.propertyItem.Value,
this.propertyItem.Len);
}
catch (ExifReaderException ex)
{
throw new InvalidOperationException(
"An ExifReaderException was caught. See InnerException for more details",
ex);
}
catch (Exception ex)
{
throw new InvalidOperationException(
"An ExifReaderException was caught. See InnerException for more details",
new ExifReaderException(ex, this.propertyFormatter, null));
}
}
Just as the code first checked for a custom property formatter in the
constructor, here it checks for a custom extractor by calling
QueryForCustomUndefinedExtractor
on the parent ExifReader
.
internal IExifValueUndefinedExtractor QueryForCustomUndefinedExtractor(
int tagId)
{
QueryUndefinedExtractorEventArgs eventArgs =
new QueryUndefinedExtractorEventArgs(tagId);
this.FireQueryUndefinedExtractor(eventArgs);
return eventArgs.UndefinedExtractor;
}
If there's no custom extractor provided,
ExifValueCreator.CreateUndefined
is called for undefined data types, and
ExifValueCreator.Create
is called for well-known data types. You
should also note how the ExifReaderException
is caught and
re-thrown in an InvalidOperationException
. This is because
InitializeExifValue
is called by the ExifProperty.ExifValue
property (shown below) and properties are not expected to throw any arbitrary
exception. Both WinForms and WPF databinding can correctly handle exceptions of
type InvalidOperationException
, hence the catch and re-throw.
public IExifValue ExifValue
{
get
{
return this.exifValue ?? this.InitializeExifValue();
}
}
The ExifValueCreator class
The ExifValueCreator
is a factory class for creating different
types of ExifValue
objects. It declares a private
delegate
with the following signature.
private delegate IExifValue CreateExifValueDelegate(
byte[] value, int length);
It also has a built-in map of Exif data types to their corresponding creation
methods.
private static Dictionary<PropertyTagType, CreateExifValueDelegate>
createExifValueDelegateMap =
new Dictionary<PropertyTagType, CreateExifValueDelegate>()
{
{ PropertyTagType.ASCII, CreateExifValueForASCIIData },
{ PropertyTagType.Byte, CreateExifValueForByteData },
{ PropertyTagType.Short, CreateExifValueForShortData },
{ PropertyTagType.Long, CreateExifValueForLongData },
{ PropertyTagType.SLONG, CreateExifValueForSLongData },
{ PropertyTagType.Rational, CreateExifValueForRationalData },
{ PropertyTagType.SRational, CreateExifValueForSRationalData }
};
The Create
method gets the appropriate method based on the
passed in Exif data type and invokes the delegate and returns its return value.
internal static IExifValue Create(
PropertyTagType type, byte[] value, int length)
{
try
{
CreateExifValueDelegate createExifValueDelegate;
if (createExifValueDelegateMap.TryGetValue(
type, out createExifValueDelegate))
{
return createExifValueDelegate(value, length);
}
return new ExifValue<string>(new[] { type.ToString() });
}
catch (Exception ex)
{
throw new ExifReaderException(ex);
}
}
Data types used by the ExifReader
Most of the Exif data types that are defined in the PropertyTagType
enum
have corresponding matches in the .NET type system. For example, the
ASCII
Exif type would be a System.String
, a Long
would be a 32 bit unsigned integer (a uint
in C#), and an
SLong
would be a 32 bit signed integer (an int
in C#). But
there are also two types that don't have direct .NET equivalents, Rational
and SRational
, and I wrote a simple Rational32
struct
that represents either of those types. I didn't want separate
struct
s for the signed and unsigned versions even though that's
what's done in the .NET framework (examples : UInt32
/Int32
,
UInt64
/Int64
). To facilitate that, I also wrote a
simple struct
called CommonInt32
that can efficiently
represent either an int
or an uint
in a mostly
transparent fashion so the caller need not unduly worry about what type is being
accessed.
[StructLayout(LayoutKind.Explicit)]
public struct CommonInt32 : IEquatable<CommonInt32>
{
[FieldOffset(0)]
private int integer;
[FieldOffset(0)]
private uint uinteger;
[FieldOffset(4)]
private bool isSigned;
public CommonInt32(int integer)
: this()
{
this.integer = integer;
this.isSigned = true;
}
public CommonInt32(uint uinteger)
: this()
{
this.uinteger = uinteger;
this.isSigned = false;
}
public bool IsSigned
{
get
{
return this.isSigned;
}
}
public static explicit operator int(CommonInt32 commonInt)
{
return commonInt.integer;
}
public static explicit operator uint(CommonInt32 commonInt)
{
return commonInt.uinteger;
}
public override bool Equals(object obj)
{
return obj is CommonInt32 && this.Equals((CommonInt32)obj);
}
public bool Equals(CommonInt32 other)
{
if (this.isSigned != other.isSigned)
{
return false;
}
return this.isSigned ?
this.integer.Equals(other.integer) :
this.uinteger.Equals(other.uinteger);
}
public override int GetHashCode()
{
return this.isSigned ?
this.integer.GetHashCode() : this.uinteger.GetHashCode();
}
}
By using a StructLayout
of LayoutKind.Explicit
and
by using FieldOffset(0)
for both the int
and the
uint
field, the struct
will not need any more space than it
needs. This is a gratuitous optimization and was not originally done for
optimization reasons, but because I was attempting an even more transparent
struct
to map between int
and uint
, but
eventually I changed other areas of my code and thus did not need that anymore.
I decided to keep the overlapping FieldOffset
scheme in since I had
already written the code. The Rational32
struct
uses
CommonInt32
fields to represent either a signed number or an
unsigned number, and thereby avoids having two separate types for this.
public struct Rational32
: IComparable, IComparable<Rational32>, IEquatable<Rational32>
{
private const char SEPARATOR = '/';
private CommonInt32 numerator;
private CommonInt32 denominator;
public Rational32(int numerator, int denominator)
: this()
{
int gcd = Rational32.EuclidGCD(numerator, denominator);
this.numerator = new CommonInt32(numerator / gcd);
this.denominator = new CommonInt32(denominator / gcd);
}
public Rational32(uint numerator, uint denominator)
: this()
{
uint gcd = Rational32.EuclidGCD(numerator, denominator);
this.numerator = new CommonInt32(numerator / gcd);
this.denominator = new CommonInt32(denominator / gcd);
}
public CommonInt32 Numerator
{
get { return this.numerator; }
}
public CommonInt32 Denominator
{
get { return this.denominator; }
}
public static explicit operator double(Rational32 rational)
{
return rational.denominator.IsSigned ?
(int)rational.denominator == 0 ?
0.0 :
(double)(int)rational.numerator /
(double)(int)rational.denominator :
(uint)rational.denominator == 0 ?
0.0 :
(double)(uint)rational.numerator /
(double)(uint)rational.denominator;
}
public static bool operator >(Rational32 x, Rational32 y)
{
return (double)x > (double)y;
}
public static bool operator >=(Rational32 x, Rational32 y)
{
return (double)x >= (double)y;
}
public static bool operator <(Rational32 x, Rational32 y)
{
return (double)x < (double)y;
}
public static bool operator <=(Rational32 x, Rational32 y)
{
return (double)x <= (double)y;
}
public override string ToString()
{
return this.denominator.IsSigned ?
String.Format("{0} {1} {2}", (int)this.numerator,
Rational32.SEPARATOR, (int)this.denominator) :
String.Format("{0} {1} {2}", (uint)this.numerator,
Rational32.SEPARATOR, (uint)this.denominator);
}
public override bool Equals(object obj)
{
return obj is Rational32 && this.Equals((Rational32)obj);
}
public bool Equals(Rational32 other)
{
return this.numerator.Equals(other.numerator) &&
this.denominator.Equals(other.denominator);
}
public override int GetHashCode()
{
int primeSeed = 29;
return unchecked((this.numerator.GetHashCode() + primeSeed) *
this.denominator.GetHashCode());
}
public int CompareTo(object obj)
{
if (obj == null)
{
return 1;
}
if (!(obj is Rational32))
{
throw new ArgumentException("Rational32 expected");
}
return this.CompareTo((Rational32)obj);
}
public int CompareTo(Rational32 other)
{
if (this.Equals(other))
{
return 0;
}
return ((double)this).CompareTo((double)other);
}
private static int EuclidGCD(int x, int y)
{
return y == 0 ? x : EuclidGCD(y, x % y);
}
private static uint EuclidGCD(uint x, uint y)
{
return y == 0 ? x : EuclidGCD(y, x % y);
}
}
Creation methods in ExifValueCreator
I have a couple of generic methods which create an IExifValue
from a given array of byte
s.
private static IExifValue CreateExifValueForGenericData<T>(
byte[] value, int length,
Func<byte[], int, T> converterFunction) where T : struct
{
int size = Marshal.SizeOf(typeof(T));
return CreateExifValueForGenericData(
value, length, size, converterFunction);
}
private static IExifValue CreateExifValueForGenericData<T>(
byte[] value, int length, int dataValueSize,
Func<byte[], int, T> converterFunction) where T : struct
{
T[] data = new T[length / dataValueSize];
for (int i = 0, pos = 0; i < length / dataValueSize;
i++, pos += dataValueSize)
{
data[i] = converterFunction(value, pos);
}
return new ExifValue<T>(data);
}
The individual creation methods will merely call one of the above two
methods. Here are a couple of examples.
private static IExifValue CreateExifValueForSRationalData(
byte[] value, int length)
{
return CreateExifValueForGenericData(
value,
length,
sizeof(int) * 2,
(bytes, pos) => new Rational32(
System.BitConverter.ToInt32(bytes, pos),
System.BitConverter.ToInt32(bytes, pos + sizeof(int))));
}
private static IExifValue CreateExifValueForLongData(
byte[] value, int length)
{
return CreateExifValueForGenericData(value, length,
(bytes, pos) => System.BitConverter.ToUInt32(bytes, pos));
}
ExifValue<T>
is a generic
class that can be used to
instantiate an IExifValue
of a specific type. If you write custom
Exif tag handlers you can use this class instead of implementing an
IExifValue
class from scratch.
public class ExifValue<T> : IExifValue
{
private T[] values;
public ExifValue(T[] values)
{
this.values = values;
}
public Type ValueType
{
get { return typeof(T); }
}
public int Count
{
get { return this.values.Length; }
}
public IEnumerable Values
{
get { return this.values.AsEnumerable(); }
}
}
Built-in extractors for undefined types
The CreateUndefined
method is used to create an IExifValue
for an undefined data type.
internal static IExifValue CreateUndefined(
PropertyTagId tagId, byte[] value, int length)
{
var extractor =
ExifValueUndefinedExtractorProvider.GetExifValueUndefinedExtractor(
tagId);
try
{
return extractor.GetExifValue(value, length);
}
catch (Exception ex)
{
throw new ExifReaderException(ex, extractor);
}
}
ExifValueUndefinedExtractorProvider
is a class that gets an
extractor object for a given tag Id. It does this by looking at the
PropertyTagId
enum
for ExifValueUndefinedExtractor
attributes. This is similar to how ExifPropertyFormatterProvider
looks for ExifPropertyFormatter
attributes.
[ExifValueUndefinedExtractor(typeof(ExifFileSourceUndefinedExtractor))]
[EnumDisplayName("File Source")]
ExifFileSource = 0xA300,
[ExifValueUndefinedExtractor(typeof(ExifSceneTypeUndefinedExtractor))]
[EnumDisplayName("Scene Type")]
ExifSceneType = 0xA301,
Here is how the ExifValueUndefinedExtractorAttribute
class is
defined.
[AttributeUsage(AttributeTargets.Field)]
internal class ExifValueUndefinedExtractorAttribute : Attribute
{
private IExifValueUndefinedExtractor undefinedExtractor;
private Type undefinedExtractorType;
public ExifValueUndefinedExtractorAttribute(Type undefinedExtractorType)
{
this.undefinedExtractorType = undefinedExtractorType;
}
public IExifValueUndefinedExtractor GetUndefinedExtractor()
{
return this.undefinedExtractor ??
(this.undefinedExtractor =
Activator.CreateInstance(this.undefinedExtractorType)
as IExifValueUndefinedExtractor);
}
}
And here's the code for ExifValueUndefinedExtractorProvider
,
which uses the CachedAttributeExtractor
utility class just as the
ExifPropertyFormatterProvider
did.
public static class ExifValueUndefinedExtractorProvider
{
internal static IExifValueUndefinedExtractor
GetExifValueUndefinedExtractor(PropertyTagId tagId)
{
ExifValueUndefinedExtractorAttribute attribute =
CachedAttributeExtractor<PropertyTagId,
ExifValueUndefinedExtractorAttribute>.Instance.GetAttributeForField(
tagId.ToString());
if (attribute != null)
{
return attribute.GetUndefinedExtractor();
}
return new SimpleUndefinedExtractor();
}
}
Here're a couple of undefined property extractors (please see the project
source for other examples).
internal class ExifFileSourceUndefinedExtractor : IExifValueUndefinedExtractor
{
public IExifValue GetExifValue(byte[] value, int length)
{
string fileSource = value.FirstOrDefault() == 3 ? "DSC" : "Reserved";
return new ExifValue<string>(new[] { fileSource });
}
}
internal class SimpleUndefinedExtractor : IExifValueUndefinedExtractor
{
public IExifValue GetExifValue(byte[] value, int length)
{
string bytesString = String.Join(" ",
value.Select(b => b.ToString("X2")));
return new ExifValue<string>(new[] { bytesString });
}
}
The SimpleUndefinedExtractor
class above is the default handler
for undefined property tags and gets used if there is no built-in extractor
available and the user has not provided a custom extractor either.
Built in formatters
The ExifReader
comes with over a couple of dozen property
formatters which cover the most commonly accessed Exif tags. As I mentioned in
the introduction, if you encounter a common Exif tag that is not handled by the
ExifReader
, I'll be glad to add support for that if you can send me
a couple of test images with that particular Exif tag. Alternatively you can
write a custom handler for it. I will list a few of the built-in formatters just
so you get an idea, and people who want to see them all can look at the project
source code.
internal class ExifISOSpeedPropertyFormatter : IExifPropertyFormatter
{
public string DisplayName
{
get
{
return "ISO Speed";
}
}
public string GetFormattedString(IExifValue exifValue)
{
var values = exifValue.Values.Cast<ushort>();
return values.Count() == 0 ?
String.Empty : String.Format("ISO-{0}", values.First());
}
}
This is a fairly simple formatter. The ISO speed is represented by a single
unsigned short
value. So all we do is extract that value and format it to a
standard ISO display string.
internal class GpsLatitudeLongitudePropertyFormatter
: SimpleExifPropertyFormatter
{
public GpsLatitudeLongitudePropertyFormatter(PropertyTagId tagId)
: base(tagId)
{
}
public override string GetFormattedString(IExifValue exifValue)
{
var values = exifValue.Values.Cast<Rational32>();
if (values.Count() != 3)
{
return String.Empty;
}
return String.Format("{0}; {1}; {2}",
(double)values.ElementAt(0),
(double)values.ElementAt(1),
(double)values.ElementAt(2));
}
}
This formatter is used by both the GpsLatitude
and the
GpsLongitude
Exif tags. Here there are three Rational
values, and you can see how the sign-transparency of the Rational32
class means we don't need to particularly check for a signed versus unsigned
number.
internal class ExifShutterSpeedPropertyFormatter : IExifPropertyFormatter
{
. . .
public string GetFormattedString(IExifValue exifValue)
{
var values = exifValue.Values.Cast<Rational32>();
if (values.Count() == 0)
{
return String.Empty;
}
double apexValue = (double)values.First();
double shutterSpeed = 1 / Math.Pow(2, apexValue);
return shutterSpeed > 1 ?
String.Format("{0} sec.", (int)Math.Round(shutterSpeed)) :
String.Format("{0}/{1} sec.", 1, (int)Math.Round(1 / shutterSpeed));
}
}
This one's similar to the ISO-speed formatter except it's a rational data
type and also needs a little math and some little formatting work applied. As I
was implementing these formatters I quickly found that a vast majority of them
required mapping from a numeric data value to a string representation. To
facilitate that, I wrote a base class that will handle the mapping, so that
implementing classes need not duplicate a lot of the code lookup logic.
internal abstract class GenericDictionaryPropertyFormatter<VTYPE>
: IExifPropertyFormatter
{
public abstract string DisplayName { get; }
public string GetFormattedString(IExifValue exifValue)
{
var values = exifValue.Values.Cast<VTYPE>();
return this.GetStringValueInternal(values.FirstOrDefault());
}
protected abstract Dictionary<VTYPE, string> GetNameMap();
protected virtual string GetReservedValue()
{
return "Reserved";
}
private string GetStringValueInternal(VTYPE value)
{
string stringValue;
if (!this.GetNameMap().TryGetValue(value, out stringValue))
{
stringValue = this.GetReservedValue();
}
return stringValue;
}
}
Here's an example of how a formatter class derives from the above class.
internal class ExifMeteringModePropertyFormatter
: GenericDictionaryPropertyFormatter<ushort>
{
private Dictionary<ushort, string> meteringModeMap
= new Dictionary<ushort, string>()
{
{ 0, "Unknown" },
{ 1, "Average" },
{ 2, "Center-Weighted" },
{ 3, "Spot" },
{ 4, "Multi-Spot" },
{ 5, "Pattern" },
{ 6, "Partial" },
{ 255, "Other" }
};
public override string DisplayName
{
get { return "Metering Mode"; }
}
protected override Dictionary<ushort, string> GetNameMap()
{
return this.meteringModeMap;
}
}
Support for the Windows Forms property-grid
The reason I added this support was because of my presumption that there will
be quite a few scenarios where a WinForms app merely wants to show all the Exif
properties in a form. And the quickest way to do it would be with a property
grid control. Supporting the PropertyGrid
was fairly
straightforward. The ExifReaderTypeDescriptionProvider
class acts
as a TypeDescriptionProvider
for the ExifReader
class.
internal class ExifReaderTypeDescriptionProvider : TypeDescriptionProvider
{
private static TypeDescriptionProvider defaultTypeProvider =
TypeDescriptor.GetProvider(typeof(ExifReader));
. . .
public override ICustomTypeDescriptor GetTypeDescriptor(
Type objectType, object instance)
{
ICustomTypeDescriptor defaultDescriptor =
base.GetTypeDescriptor(objectType, instance);
return instance == null ? defaultDescriptor :
new ExifReaderCustomTypeDescriptor(defaultDescriptor, instance);
}
}
The ExifReaderCustomTypeDescriptor
instance returned above
implements a custom TypeDescriptor
for the ExifReader
class.
internal class ExifReaderCustomTypeDescriptor : CustomTypeDescriptor
{
private List<PropertyDescriptor> customFields
= new List<PropertyDescriptor>();
public ExifReaderCustomTypeDescriptor(
ICustomTypeDescriptor parent, object instance)
: base(parent)
{
ExifReader exifReader = (ExifReader)instance;
this.customFields.AddRange(
exifReader.GetExifProperties().Select(
ep => new ExifPropertyPropertyDescriptor(ep)));
}
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(
base.GetProperties().Cast<PropertyDescriptor>().Union(
this.customFields).ToArray());
}
public override PropertyDescriptorCollection GetProperties(
Attribute[] attributes)
{
return new PropertyDescriptorCollection(
base.GetProperties(attributes).Cast<PropertyDescriptor>().Union(
this.customFields).ToArray());
}
}
For every Exif property the reader returns, a
ExifPropertyPropertyDescriptor
is created and added to the property list.
It's ExifPropertyPropertyDescriptor
which provides the property
name, property value, property type etc. for the PropertyGrid
.
internal class ExifPropertyPropertyDescriptor : PropertyDescriptor
{
public ExifPropertyPropertyDescriptor(ExifProperty exifProperty)
: base(exifProperty.ExifPropertyName, new Attribute[1]
{ new CategoryAttribute(exifProperty.ExifPropertyCategory) })
{
this.ExifProperty = exifProperty;
}
public ExifProperty ExifProperty { get; private set; }
public override Type ComponentType
{
get { return typeof(ExifReader); }
}
. . .
public override Type PropertyType
{
get { return typeof(string); }
}
. . .
public override object GetValue(object component)
{
return this.ExifProperty.ToString();
}
. . .
}
In the partially snipped code listing, I have highlighted the important
areas. We use ExifProperty.ExifPropertyName
as the name of the
property, and set the type of the property as a String
. And finally
for the property value, we call ToString()
on the
ExifProperty
member. I thought of specifying actual data types instead of
making them all String
s, but since this is a reader class and there
is no plan for writer-support in the foreseeable future, I didn't think it worth
the effort. And obviously PropertyGrid
support is a convenience and
not a necessity, and there are far more flexible ways to use the class as shown
in the WPF demo application.
Thank you if you made it this far, and please feel free to pass on your
criticism and other feedback via the forum attached to this article.
History
- March 28, 2010 - Article first published