Click here to Skip to main content
12,758,462 members (30,062 online)
Click here to Skip to main content
Add your own
alternative version


41 bookmarked
Posted 11 Apr 2010

EXIF Compare Utility using WPF

, 12 Apr 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
The Exif Compare Utility is a WinDiff equivalent for image files that compares the Exif meta-data and displays the differences and similarities. The application is written using WPF and MVVM.

Figure 1: The application in action - all sections expanded (though not visible in the screenshot)

Figure 2: Screenshot showing collapsed sections and the ability to copy text via the context menu


The Exif Compare Utility is an application I wrote for my specific needs. It is a WinDiff equivalent for image files that compares the Exif meta-data and displays the differences and similarities. The application is written using WPF and MVVM, and also makes use of my ExifReader library. In the article I will briefly explain how to use the application and also discuss some of the interesting code implementation details.

Code Formatting

The code snippets in the article body have been ruthlessly word wrapped so they fit within the 600 pixel width requirement that Code Project has. The code formatting in the actual source files is way more pleasant.

Using the utility

Choose Compare Files either from the menu or from the toolbar, and you'll get two File Open dialogs which'll let you choose two image files. Once you choose the files you will see the Exif comparison results (see Figure 1) via four collapsible sections.

  1. Differing Exif properties
  2. Exif properties only in left image
  3. Exif properties only in right image
  4. Identical Exif properties

Use Clear Files (menu/toolbar) to clear the selections. You can right click on an entry to bring up the context menu (see Figure 2) which has just one entry - a Copy command that copies a textual representation to the clipboard.

Interesting coding details

The WPF application was written using VS 2010 RC and uses the MVVM pattern. It uses my ExifReader library for reading the Exif data, but implements the comparison in the model class. I will be referring to a couple of classes from the ExifReader library, and to avoid repetition I will not describe them in this article. Those of you who want to look it up can read the ExifReader article.  Note that the source download for this article does include the ExifReader project too, so you won't need any additional downloads to compile and run the application.


There is a class called ExifPropertyComparison which represents a particular Exif property. It will include either the left or right values, or both depending on whether a property exists in both images or just one.

internal class ExifPropertyComparison
    private const string EMPTYSTRING = "N/A";

    public ExifPropertyComparison(ExifProperty exifProperty)
        : this(exifProperty, exifProperty)

    public ExifPropertyComparison(ExifProperty left, ExifProperty right)
        if (left == null && right == null)
            throw new InvalidOperationException(
                "Both arguments cannot be null.");

        if (left == null)
            this.IsLeftEmpty = true;
            this.LeftValue = EMPTYSTRING;
            this.LeftValue = left.ToString();

        if (right == null)
            this.IsRightEmpty = true;
            this.RightValue = EMPTYSTRING;
            this.RightValue = right.ToString();

        this.TagName = (left ?? right).ExifTag.ToString();

    public bool IsLeftEmpty { get; private set; }

    public bool IsRightEmpty { get; private set; }

    public string LeftValue { get; private set; }

    public string RightValue { get; private set; }

    public string TagName { get; private set; }

A couple of properties in there (IsLeftEmpty and IsRightEmpty) are provided for data binding convenience. Some of you may be wondering if this breaks the MVVM model, since we have properties in a Model class purely for the View's convenience. But remember that the View has absolutely no idea of this Model class. The data association is done at run-time by the WPF data-binding framework. You could move this class to the View-Model too if you are that pedantic about these things, but I personally prefer to choose the simplest option to more doctrinaire and convoluted ones.

The property comparison is based on comparing the Exif tag as well as the Exif property value. Since both the ExifReader and ExifProperty classes do not support equality checks, I implemented the following comparer classes.

internal class ExifPropertyTagEqualityComparer 
    : IEqualityComparer<ExifProperty>
    public bool Equals(ExifProperty x, ExifProperty y)
        return x.ExifTag == y.ExifTag;

    public int GetHashCode(ExifProperty obj)
        return (int)obj.ExifTag;

This one is fairly simple, as all it does it compare the ExifTag properties.

internal class ExifPropertyValueEqualityComparer 
    : IEqualityComparer<ExifProperty>

    public bool Equals(ExifProperty x, ExifProperty y)
        if (x.ExifValue.Count != y.ExifValue.Count 
            || x.ExifDatatype != y.ExifDatatype)
                <span class="code-keyword">return false;

        bool equal = true;

        object[] xValues = x.ExifValue.Values.Cast<object>().ToArray();
        object[] yValues = y.ExifValue.Values.Cast<object>().ToArray();

        for (int i = 0; i < xValues.Length; i++)
            if (!(equal = xValues[i].Equals(yValues[i])))

Comparing the Exif value needs a little more code since there can often be multiple values. Once the comparers are available, writing the actual Exif comparison code is fairly easy, with a bit of LINQ usage. This is implemented in the ExifCompareModel class.

internal class ExifCompareModel
    private static ExifPropertyTagEqualityComparer tagEqualityComparer 
        = new ExifPropertyTagEqualityComparer();

    private static ExifPropertyValueEqualityComparer valueEqualityComparer 
        = new ExifPropertyValueEqualityComparer();

    private List<ExifPropertyComparison> onlyInLeftProperties 
        = new List<ExifPropertyComparison>();

    public List<ExifPropertyComparison> OnlyInLeftProperties
        get { return onlyInLeftProperties; }

    private List<ExifPropertyComparison> onlyInRightProperties 
        = new List<ExifPropertyComparison>();

    public List<ExifPropertyComparison> OnlyInRightProperties
        get { return onlyInRightProperties; }

    private List<ExifPropertyComparison> identicalProperties 
        = new List<ExifPropertyComparison>();

    public List<ExifPropertyComparison> IdenticalProperties
        get { return identicalProperties; }

    private List<ExifPropertyComparison> differingProperties 
        = new List<ExifPropertyComparison>();

    public List<ExifPropertyComparison> DifferingProperties
        get { return differingProperties; }

    /// <span class="code-SummaryComment"><summary>

The class exposes four properties of type List<ExifPropertyComparison> representing the four categories of properties - those properties only in either the left or the right image, those in both images and with identical values, and those in both but with different values.

View Model

Here's the code (partly snipped) for the main window's View-Model.

internal class MainWindowViewModel : ViewModelBase
    . . .

    private ImageUserControlViewModel leftImageUserControlViewModel 
      = ImageUserControlViewModel.Empty;

    private ImageUserControlViewModel rightImageUserControlViewModel 
      = ImageUserControlViewModel.Empty;

    /// <span class="code-SummaryComment"><summary>

The View-Model exposes four ObservableCollection<ExifPropertyComparison> properties which it populates from the equivalent properties returned from the Model. The preview image views have their own View-Model and these are returned via the LeftImageUserControlViewModel and RightImageUserControlViewModel properties. Notice the code that brings up the About dialog, where the Owner window is set through the command-parameter argument. I'll talk about this later when I show the corresponding View code.

internal class ImageUserControlViewModel : ViewModelBase
    . . .

    private static ImageUserControlViewModel empty = 
        new ImageUserControlViewModel();

    public static ImageUserControlViewModel Empty
        get { return ImageUserControlViewModel.empty; }

    private ImageUserControlViewModel()

    /// <span class="code-SummaryComment"><summary>

Theoretically I could have put all this in the main view-model, but I did this for convenience and for simpler code organization. In addition, there's a View-Model class for the About dialog where information is shown based on the assembly's version information. I used a fairly common MVVM technique of raising an event in the View-Model that's handled by the View to close the About dialog. People like Josh Smith have blogged and written about this technique in various articles. This is in contrast to the technique I used to pass the owner Window to the code that brings up the About dialog. I could have used the same technique here, but I chose to do it this way to get a feel for either techniques. The negative in this latter approach is that you need to have code in the View class, which may offend MVVM purists, though strictly speaking the only thing that code does is close the dialog. With the former technique there's no code needed in the View, but it's some rather inelegant binding code that's used to pass the Owner window as a command parameter, which some people may not want to do. Here's the code for the About dialog View-Model.

internal class AboutWindowViewModel : ViewModelBase
    private FileVersionInfo fileVersionInfo;

    private ICommand closeCommand;

    /// <span class="code-SummaryComment"><summary>


There's nothing of interest the About dialog Xaml except for the code in the constructor that handles the View-Model event (that I talked about earlier).

public AboutWindow()

    var viewModel = new AboutWindowViewModel();
    viewModel.CloseRequested += (s, e) => Close();
    this.DataContext = viewModel;

Here's the artificially word-wrapped Xaml for the ImageUserControl view class.

<UserControl x:Class="ExifCompare.ImageUserControl"
     . . .
     d:DesignHeight="264" d:DesignWidth="320" Background="Transparent">
<dropShadow:SystemDropShadowChrome CornerRadius="15, 15, 0, 0" Width="320">
    <Grid Background="Transparent">
        <<span class="code-leadattribute">StackPanel Orientation="Vertical" Tag="{Binding}">

Look at the highlighted code where the tool-tip is setup. Initially I didn't have code that looked like that, instead I merely bound to the current object so I would just see the ToString result of the ImageUserControlViewModel object. I found that this only worked the first time the code was called, and when I opened fresh files, the tool-tip did not update. It took me a while and some Googling before I realized that the ToolTip will not inherit the data-context since it was not part of the visual tree. So in this case, the PlacementTarget will be the containing StackPanel and I bind its Tag property to the ImageUserControlViewModel  object, which is what the tool-tip gets too when it pops up and invokes data-binding.  Problem solved.

Here's the Xaml for the main window. Some of the word wrapping where a binding string is wrapped may actually break the code. But then I don't expect anyone to copy/paste this code into an IDE - since you can always look at the provided source download.

<nsmvvm:SystemMenuWindow x:Class="ExifCompare.MainWindow" 
        . . .                      
        Title="Exif Compare Utility" Height="750" Width="1000" 
        MinHeight="300" MinWidth="750">


                <ResourceDictionary Source="ExifCompareResourceDictionary.xaml" />


        <nsmvvm:SystemMenuItem Command="{Binding AboutCommand}" 
           <span class="code-attribute">CommandParameter="{Binding RelativeSource=
            {RelativeSource Mode=FindAncestor, AncestorType=Window}}" </span>
           Header="About" Id="100" />

I've used my SystemMenuWindow class to add the About menu entry to the system window. But the interesting code there is the CommandParameter binding. I find the first Window ancestor and pass that to the command, since the About dialog needs to set an owner window for centering. This way we avoid a back-reference to the View from the View-Model, even though there are MVVM implementations where the View-Model has a reference to the View. I could also have implemented an event in the View-Model that the View handles to pass in an owner, but I thought this was simpler to do.

            <RowDefinition Height="60" />
            <RowDefinition />

        <Grid Grid.Row="0">
            <Menu Height="25" VerticalAlignment="Top">
                <MenuItem Header="File">
                    <<span class="code-leadattribute">MenuItem Command="{Binding CompareFilesCommand}" </span>
                      Header="Compare Files" />

The menu and toolbar implement the same functionality and use the same View-Model commands.

        <ScrollViewer Grid.Row="1">
                <LinearGradientBrush EndPoint="0,0" StartPoint="1,0">
                    <GradientStop Color="#FFA7B7A7" Offset="0" />
                    <GradientStop Color="#FF195219" Offset="1" />
            <Grid Margin="20">
                <StackPanel Orientation="Vertical">
                    <Expander VerticalAlignment="Top" Header="Preview Images"
                      IsExpanded="True" ExpandDirection="Down" 
                        FontSize="16" FontWeight="Bold">
                        <Grid VerticalAlignment="Stretch" 
                            <StackPanel Orientation="Horizontal" 
                              Height="260" VerticalAlignment="Top" 
                      <span class="code-attribute">DataContext="{Binding LeftImageUserControlViewModel}" </span>
                      Margin="5" />

The image preview user controls specify the DataContext in the Xaml and bind to the appropriate View-Model objects.

                    <Expander VerticalAlignment="Top" 
                    Header="Differing EXIF properties"
                      IsExpanded="True" ExpandDirection="Down" 
                      FontSize="16" FontWeight="Bold">
                        <Grid VerticalAlignment="Stretch" MinHeight="40" 
                              Background="#FFB7D4B7" TextBlock.FontSize="13" 
                            <<span class="code-leadattribute">ListBox ItemsSource="{Binding DifferingProperties}" 
                                     Style="{StaticResource ExifListBox}"
                   ItemTemplate="{StaticResource ExifListBoxItemTemplate}"
                   ItemContainerStyle="{StaticResource ExifListBoxItem}">

The property comparisons are shown using styled and templated list-boxes. They are all wrapped inside Expander blocks, and the Expander is also styled differently since the default look did not look natural - given the rest of the styling/theming. I'll briefly go through some of the styles and templates that are defined in a separate resource dictionary.

Styles and Templates

For customizing the Expander control, I started off with this MSDN example. I removed some of the bits (example, those that dealt with the disabled state) and then customized it to my preference.

<ControlTemplate x:Key="ExpanderToggleButton" 
    <Border Name="Border" 
                Background="{StaticResource BasicBrush}"
                BorderBrush="{StaticResource NormalBorderBrush}"
        <Path Name="Arrow"
                  Fill="{StaticResource GlyphBrush}"
                  Data="M 0 0 L 8 8 L 16 0 Z"/>
        <Trigger Property="ToggleButton.IsMouseOver" Value="true">
            <Setter TargetName="Border" Property="Background" 
                Value="{StaticResource DarkBrush}" />
        <Trigger Property="IsPressed" Value="true">
            <Setter TargetName="Border" Property="Background" 
                Value="{StaticResource PressedBrush}" />
        <Trigger Property="IsChecked" Value="true">
            <Setter TargetName="Arrow" Property="Data" 
                Value="M 0 8 L 8 0 L 16 8 Z" />

This is the control template for the ToggleButton on the Expander control. The arrow, and its flipped version are both created using basic Path controls.

<Style TargetType="Expander">
<Setter Property="Template">
<ControlTemplate TargetType="Expander">
    . . .
    <Border Name="Border" 
                Background="{StaticResource LightBrush}"
                BorderBrush="{StaticResource NormalBorderBrush}"
                CornerRadius="2,2,0,0" >
                <ColumnDefinition Width="25" />
                <ColumnDefinition Width="*" />
            <<span class="code-leadattribute">ToggleButton 
        IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,
              RelativeSource={RelativeSource TemplatedParent}}"
        Template="{StaticResource ExpanderToggleButton}" />

In the control template for the Expander, we use the customized ToggleButton template.

<Style TargetType="{x:Type ListBox}" x:Key="ExifListBox">
. . .
<Setter Property="Template">
          <ItemsPresenter />

  <<span class="code-leadattribute">DataTrigger Binding="{Binding RelativeSource={x:Static 
    RelativeSource.Self}, Path=Items.Count}" Value="0">

Here's a style I use on the ListBox, so that when the ListBox is empty I can show a custom empty message. This works because of the binding where I bind to the Count property of the Items property.

<DataTemplate x:Key="ExifListBoxItemTemplate">
<Grid Background="#FF6E9A96" HorizontalAlignment="Stretch" 
    <span class="code-attribute">MaxWidth="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
        AncestorType=ListBox}, Path=ActualWidth}">

Here's the data template for the ListBoxItem - this is where we actually display the property comparison data. There are some Exif properties where the ToString implementation in the ExifProperty class returns a display string of hex bytes. This resulted in horizontal scrolling, and to avoid this I make sure using a binding that the MaxWidth is equal to the present width of the ListBox. That's the highlighted code on top.

The data triggers show how the IsLeftEmpty and IsRightEmpty properties are used to change the opacity based on whether a property is available or not. This was what I was referring to earlier when I discussed the Model implementation.

<ContextMenu x:Key="ListBoxItemContextMenu">
<MenuItem Header="Copy" 
          <span class="code-attribute">Command="{Binding RelativeSource={
            RelativeSource Mode=FindAncestor, 
            AncestorType=ListBox}, Path=DataContext.ExifCompareCopy}"

This was another piece of code where I stumbled for a few minutes. The ListBoxItem's data context will be the ExifPropertyComparison object associated with it. Since I wanted to bind to a command in the View-Model, I had to first fetch the correct data source by using FindAncestor to lookup the data context associated with the ListBox (which will be the View-Model instance).


I guess there wasn't anything earth shattering in this article, but I was primarily trying to discuss issues I ran into and how I solved them. Obviously if you think there are better ways to solve these issues, please do use the article forum to put forward your suggestions, criticism, and ideas. Thank you.


  • April 10, 2009 - Article first published


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Nish Nishant
United States United States
Nish Nishant is the Principal Software Architect/Consultant for Ganymede Software Solutions LLC, and is based out of Columbus, Ohio. He has over 17 years of software industry experience in various roles including Lead Software Architect, Principal Software Engineer, and Product Manager. Nish was a Microsoft Visual C++ MVP between 2002 and 2015.

Nish is an industry acknowledged expert in the Microsoft technology stack. He authored C++/CLI in Action for Manning Publications in 2005, and had previously co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on and another 250+ blog articles on his WordPress blog. Nish is vastly experienced in team management, mentoring teams, and directing all stages of software development.

Contact Nish : If you are interested in hiring Nish as a consultant, you can reach him via his google email id voidnish.

Company Website :

You may also be interested in...


Comments and Discussions

QuestionRE-Upload again? Pin
Member 865604831-Oct-12 6:14
memberMember 865604831-Oct-12 6:14 
AnswerRe: RE-Upload again? Pin
Nish Sivakumar31-Oct-12 6:16
mvpNish Sivakumar31-Oct-12 6:16 
GeneralRe: RE-Upload again? Pin
Member 865604831-Oct-12 11:21
memberMember 865604831-Oct-12 11:21 
GeneralRe: RE-Upload again? Pin
Nish Sivakumar1-Nov-12 5:41
mvpNish Sivakumar1-Nov-12 5:41 
QuestionAny /Exe available anywhere for your Exif compare prog? Pin
crell7-Jul-10 2:52
membercrell7-Jul-10 2:52 
AnswerRe: Any /Exe available anywhere for your Exif compare prog? Pin
snowman17-Oct-10 10:03
membersnowman17-Oct-10 10:03 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pin
Nishant Sivakumar7-Oct-10 10:09
mvpNishant Sivakumar7-Oct-10 10:09 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pin
snowman17-Oct-10 10:17
membersnowman17-Oct-10 10:17 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pin
Nishant Sivakumar7-Oct-10 10:20
mvpNishant Sivakumar7-Oct-10 10:20 
AnswerRe: Any /Exe available anywhere for your Exif compare prog? Pin
Nishant Sivakumar7-Oct-10 10:08
mvpNishant Sivakumar7-Oct-10 10:08 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pin
Wolfram Kuss2-Sep-11 1:46
memberWolfram Kuss2-Sep-11 1:46 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pin
Nishant Sivakumar2-Sep-11 2:30
mvpNishant Sivakumar2-Sep-11 2:30 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pin
Wolfram Kuss2-Sep-11 22:24
memberWolfram Kuss2-Sep-11 22:24 
GeneralNice, Can't belive I missed this! Pin
Alan Beasley14-Apr-10 9:00
memberAlan Beasley14-Apr-10 9:00 
GeneralRe: Nice, Can't belive I missed this! Pin
Nishant Sivakumar14-Apr-10 9:08
mvpNishant Sivakumar14-Apr-10 9:08 
GeneralRe: Nice, Can't belive I missed this! Pin
Alan Beasley14-Apr-10 9:31
memberAlan Beasley14-Apr-10 9:31 
GeneralRe: Nice, Can't belive I missed this! Pin
Nishant Sivakumar14-Apr-10 9:36
mvpNishant Sivakumar14-Apr-10 9:36 
GeneralRe: Nice, Can't belive I missed this! Pin
Alan Beasley14-Apr-10 9:54
memberAlan Beasley14-Apr-10 9:54 
GeneralRe: Nice, Can't belive I missed this! Pin
Alan Beasley14-Apr-10 10:23
memberAlan Beasley14-Apr-10 10:23 
GeneralRe: Nice, Can't belive I missed this! Pin
Nishant Sivakumar14-Apr-10 10:36
mvpNishant Sivakumar14-Apr-10 10:36 
GeneralRe: Nice, Can't belive I missed this! Pin
Alan Beasley14-Apr-10 10:59
memberAlan Beasley14-Apr-10 10:59 
GeneralRe: Nice, Can't belive I missed this! Pin
Nishant Sivakumar14-Apr-10 11:01
mvpNishant Sivakumar14-Apr-10 11:01 
GeneralRe: Nice, Can't belive I missed this! Pin
Alan Beasley14-Apr-10 11:17
memberAlan Beasley14-Apr-10 11:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170217.1 | Last Updated 12 Apr 2010
Article Copyright 2010 by Nish Nishant
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid