Click here to Skip to main content
Click here to Skip to main content

EXIF Compare Utility using WPF

, 12 Apr 2010
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

Introduction

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.

Model

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;
        }
        else
        {
            this.LeftValue = left.ToString();
        }

        if (right == null)
        {
            this.IsRightEmpty = true;
            this.RightValue = EMPTYSTRING;
        }
        else
        {
            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])))
            {
                break;
            }
        }</span>

        return equal;
    }

    public int GetHashCode(ExifProperty obj)
    {
        return (int)obj.ExifTag * (int)obj.ExifDatatype 
            + obj.ExifValue.Count;
    }
}

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>
</span>    /// Initializes a new instance of the ExifCompareModel class.
    /// <span class="code-SummaryComment"></summary>
</span>    public ExifCompareModel(string leftFileName, string rightFileName)
    {
        var leftproperties = new ExifReader(
            leftFileName).GetExifProperties();
        var rightproperties = new ExifReader(
            rightFileName).GetExifProperties();

        var onlyInLeft = leftproperties.Except(
            rightproperties, tagEqualityComparer);
        <span class="code-keyword">this.onlyInLeftProperties = onlyInLeft.Select(
            p => new ExifPropertyComparison(p, null)).ToList();</span>

        var onlyInRight = rightproperties.Except(
            leftproperties, tagEqualityComparer);
        <span class="code-keyword">this.onlyInRightProperties = onlyInRight.Select(
            p => new ExifPropertyComparison(null, p)).ToList();</span>

        var commonpropertiesInLeft = leftproperties.Except(
            onlyInLeft, tagEqualityComparer).OrderBy(
            exprop => exprop.ExifTag).ToArray();
        var commonpropertiesInRight = rightproperties.Except(
            onlyInRight, tagEqualityComparer).OrderBy(
            exprop => exprop.ExifTag).ToArray();
        
        for (int i = 0; i < commonpropertiesInLeft.Length; i++)
        {
            if (valueEqualityComparer.Equals(
                commonpropertiesInLeft[i], commonpropertiesInRight[i]))
            {
                <span class="code-keyword">this.identicalProperties.Add(
                    new ExifPropertyComparison(commonpropertiesInLeft[i]));
            }
            else
            {
                this.differingProperties.Add(
                    new ExifPropertyComparison(
                        commonpropertiesInLeft[i], 
                        commonpropertiesInRight[i]));
            }</span>
        }
    }
}

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>
</span>    /// Initializes a new instance of the MainWindowViewModel class.
    /// <span class="code-SummaryComment"></summary>
</span>    public MainWindowViewModel()
    {
        this.OnlyInLeftProperties = 
          new ObservableCollection<ExifPropertyComparison>();
        this.OnlyInRightProperties = 
          new ObservableCollection<ExifPropertyComparison>();
        this.IdenticalProperties = 
          new ObservableCollection<ExifPropertyComparison>();
        this.DifferingProperties = 
          new ObservableCollection<ExifPropertyComparison>();
    }

    public ObservableCollection<ExifPropertyComparison> 
        OnlyInLeftProperties { <span class="code-keyword">get; private set; }

    public ObservableCollection<ExifPropertyComparison> 
        OnlyInRightProperties { get; private set; }

    public ObservableCollection<ExifPropertyComparison> 
        IdenticalProperties { get; private set; }

    public ObservableCollection<ExifPropertyComparison> 
        DifferingProperties { get; private set; }</span>

    public ICommand ExitCommand
    {
        get
        {
            return exitCommand ?? 
                (exitCommand = new DelegateCommand(
                    () => Application.Current.Shutdown()));
        }
    }

    public ICommand CompareFilesCommand
    {
        get
        {
            return compareFilesCommand ?? 
                (compareFilesCommand = new DelegateCommand(BrowseForFiles));
        }
    }

    public ICommand ClearFilesCommand
    {
        get
        {
            return clearFilesCommand ?? 
                (clearFilesCommand = 
                    new DelegateCommand(ClearFiles, CanClearFiles));
        }
    }

    public ICommand AboutCommand
    {
        get
        {
            return aboutCommand ?? 
                (aboutCommand = 
                    <span class="code-keyword">new DelegateCommand<Window>(
                        (owner) => new AboutWindow() 
                            { Owner = owner }.ShowDialog()));</span>
        }
    }

    public ICommand ExifCompareCopy
    {
        get
        {
            return exifCompareCopy ?? 
                (exifCompareCopy = 
                    new DelegateCommand<ExifPropertyComparison>(
                        ExifCompareCopyToClipBoard));
        }
    }

    public ImageUserControlViewModel LeftImageUserControlViewModel
    {
        get
        {
            return leftImageUserControlViewModel;
        }

        set
        {
            if (this.leftImageUserControlViewModel.FilePath 
                != value.FilePath)
            {
                this.leftImageUserControlViewModel = value;
                this.FirePropertyChanged("LeftImageUserControlViewModel");
            }
        }
    }

    public ImageUserControlViewModel RightImageUserControlViewModel
    {
        get
        {
            return rightImageUserControlViewModel;
        }

        set
        {
            if (this.rightImageUserControlViewModel.FilePath 
                != value.FilePath)
            {
                this.rightImageUserControlViewModel = value;
                this.FirePropertyChanged("RightImageUserControlViewModel");
            }
        }
    }


    public void BrowseForFiles()
    {
        OpenFileDialog fileDialog = new OpenFileDialog()
        {
            Filter = "Image Files(*.PNG;*.JPG)|*.PNG;*.JPG;"
        };

        if (fileDialog.ShowDialog().GetValueOrDefault())
        {
            string tempLeftFilePath = fileDialog.FileName;

            if (fileDialog.ShowDialog().GetValueOrDefault())
            {
                try
                {
                    ExifCompareModel exifCompare = new ExifCompareModel(
                        tempLeftFilePath, fileDialog.FileName);

                    Repopulate(this.OnlyInLeftProperties, 
                        exifCompare.OnlyInLeftProperties);
                    Repopulate(<span class="code-keyword">this.OnlyInRightProperties, 
                        exifCompare.OnlyInRightProperties);
                    Repopulate(this.IdenticalProperties, 
                        exifCompare.IdenticalProperties);
                    Repopulate(this.DifferingProperties, 
                        exifCompare.DifferingProperties);</span>

                    this.LeftImageUserControlViewModel = 
                      new ImageUserControlViewModel(tempLeftFilePath);
                    this.RightImageUserControlViewModel = 
                      new ImageUserControlViewModel(fileDialog.FileName);
                }
                catch (ExifReaderException ex)
                {
                    MessageBox.Show(ex.Message, "Error reading EXIF data", 
                        MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }
    }

    public void ExifCompareCopyToClipBoard(
        ExifPropertyComparison parameter)
    {
        var stringData = String.Format(
            "TagName = {0}, LeftValue = {1}, RightValue = {2}", 
            parameter.TagName, parameter.LeftValue, parameter.RightValue);

        Clipboard.SetText(stringData);
    }

    private void Repopulate(
      ObservableCollection<ExifPropertyComparison> observableCollection, 
      List<ExifPropertyComparison> list)
    {
        observableCollection.Clear();
        list.ForEach(p => observableCollection.Add(p));
    }

    public void ClearFiles()
    {
        this.OnlyInLeftProperties.Clear();
        this.OnlyInRightProperties.Clear();
        this.IdenticalProperties.Clear();
        this.DifferingProperties.Clear();

        this.LeftImageUserControlViewModel = ImageUserControlViewModel.Empty;
        this.RightImageUserControlViewModel = ImageUserControlViewModel.Empty;
    }

    public bool CanClearFiles()
    {
        return this.LeftImageUserControlViewModel 
          != ImageUserControlViewModel.Empty;             
    }

}

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>
</span>    /// Initializes a new instance of the ImageUserControlViewModel class.
    /// <span class="code-SummaryComment"></summary>
</span>    public ImageUserControlViewModel(string filePath)
    {
        this.fileName = Path.GetFileName(filePath);
        this.filePath = filePath;
    }

    public string FileName
    {
        get
        {
            return fileName ?? "No image selected";
        }
    }

    public string FilePath
    {
        get
        {
            return filePath;
        }
    }

    public override string ToString()
    {
        return FilePath ?? "No image selected";
    }
}

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>
</span>    /// Initializes a new instance of the AboutWindowViewModel class.
    /// <span class="code-SummaryComment"></summary>
</span>    public AboutWindowViewModel()
    {
        fileVersionInfo = FileVersionInfo.GetVersionInfo(
            Assembly.GetExecutingAssembly().Location);
    }

    public event EventHandler CloseRequested;

    private <span class="code-keyword">void FireCloseRequested()
    {
        EventHandler handler = CloseRequested;

        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }</span>

    public ICommand CloseCommand
    {
        get
        {
            return closeCommand ?? (
              closeCommand = <span class="code-keyword">new DelegateCommand(() => FireCloseRequested()));</span>
        }
    }

    public string Comments
    {
        get
        {
            return fileVersionInfo.Comments;
        }
    }

    public string InternalName
    {
        get
        {
            return fileVersionInfo.InternalName;
        }
    }

    public string FileDescription
    {
        get
        {
            return fileVersionInfo.FileDescription;
        }
    }

    public string FileVersion
    {
        get
        {
            return fileVersionInfo.FileVersion;
        }
    }

    public string LegalCopyright
    {
        get
        {
            return fileVersionInfo.LegalCopyright;
        }
    }
}

View

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()
{
    InitializeComponent();

    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">

    <Window.Resources>

        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="ExifCompareResourceDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>

    </Window.Resources>

    <nsmvvm:SystemMenuWindow.MenuItems>
        <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.

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="60" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <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">
            <ScrollViewer.Background>
                <LinearGradientBrush EndPoint="0,0" StartPoint="1,0">
                    <GradientStop Color="#FFA7B7A7" Offset="0" />
                    <GradientStop Color="#FF195219" Offset="1" />
                </LinearGradientBrush>
            </ScrollViewer.Background>            
            
            <Grid Margin="20">
                <StackPanel Orientation="Vertical">
                    <Expander VerticalAlignment="Top" Header="Preview Images"
                      IsExpanded="True" ExpandDirection="Down" 
                        FontSize="16" FontWeight="Bold">
                        <Grid VerticalAlignment="Stretch" 
                        Background="#FFB7D4B7">
                            <StackPanel Orientation="Horizontal" 
                              Height="260" VerticalAlignment="Top" 
                              HorizontalAlignment="Center">
                                <local:ImageUserControl 
                      <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" 
                              TextBlock.FontWeight="Normal">
                            <<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" 
    TargetType="ToggleButton">
    <Border Name="Border" 
                CornerRadius="2,0,0,0"
                Background="{StaticResource BasicBrush}"
                BorderBrush="{StaticResource NormalBorderBrush}"
                BorderThickness="0,0,1,0">
        <Path Name="Arrow"
                  Fill="{StaticResource GlyphBrush}"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center"
                  Data="M 0 0 L 8 8 L 16 0 Z"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="ToggleButton.IsMouseOver" Value="true">
            <Setter TargetName="Border" Property="Background" 
                Value="{StaticResource DarkBrush}" />
        </Trigger>
        <Trigger Property="IsPressed" Value="true">
            <Setter TargetName="Border" Property="Background" 
                Value="{StaticResource PressedBrush}" />
        </Trigger>
        <Trigger Property="IsChecked" Value="true">
            <Setter TargetName="Arrow" Property="Data" 
                Value="M 0 8 L 8 0 L 16 8 Z" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

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">
<Setter.Value>
<ControlTemplate TargetType="Expander">
<Grid>
    . . .
    <Border Name="Border" 
                Grid.Row="0" 
                Background="{StaticResource LightBrush}"
                BorderBrush="{StaticResource NormalBorderBrush}"
                BorderThickness="1" 
                CornerRadius="2,2,0,0" >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="25" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <<span class="code-leadattribute">ToggleButton 
        IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,
              RelativeSource={RelativeSource TemplatedParent}}"
        OverridesDefaultStyle="True" 
        Template="{StaticResource ExpanderToggleButton}" /></span>
  
            <ContentPresenter Grid.Column="1"
                                  Margin="4" 
                                  ContentSource="Header" 
                                  RecognizesAccessKey="True" />
        </Grid>
    </Border>
    <Border Name="Content" 
                Grid.Row="1" 
                Background="{StaticResource WindowBackgroundBrush}"
                BorderBrush="{StaticResource SolidBorderBrush}" 
                BorderThickness="1,0,1,1" 
                CornerRadius="0,0,2,2" >
        <ContentPresenter />
    </Border>
</Grid>
<ControlTemplate.Triggers>
    <Trigger Property="IsExpanded" Value="True">
        <Setter TargetName="ContentRow" Property="Height" 
          Value="{Binding ElementName=Content,Path=DesiredHeight}" />
    </Trigger>
</ControlTemplate.Triggers>
. . .

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

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

<Style.Triggers>
  <<span class="code-leadattribute">DataTrigger Binding="{Binding RelativeSource={x:Static 
    RelativeSource.Self}, Path=Items.Count}" Value="0"></span>
      <Setter Property="Template">
          <Setter.Value>
              <ControlTemplate>
                  <<span class="code-leadattribute">TextBlock Foreground="#FF183E11" 
                    FontSize="14">

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" 
    x:Name="mainItemGrid"
    TextBlock.FontSize="15"
    <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}"
          CommandParameter="{Binding}"/>

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).

Conclusion

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.

History

  • April 10, 2009 - Article first published

License

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

Share

About the Author

Nish Sivakumar

United States United States
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Comments and Discussions

 
QuestionRE-Upload again? PinmemberMember 865604831-Oct-12 5:14 
AnswerRe: RE-Upload again? PinmvpNish Sivakumar31-Oct-12 5:16 
GeneralRe: RE-Upload again? PinmemberMember 865604831-Oct-12 10:21 
GeneralRe: RE-Upload again? PinmvpNish Sivakumar1-Nov-12 4:41 
QuestionAny /Exe available anywhere for your Exif compare prog? Pinmembercrell7-Jul-10 1:52 
AnswerRe: Any /Exe available anywhere for your Exif compare prog? Pinmembersnowman17-Oct-10 9:03 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? PinmvpNishant Sivakumar7-Oct-10 9:09 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? Pinmembersnowman17-Oct-10 9:17 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? PinmvpNishant Sivakumar7-Oct-10 9:20 
AnswerRe: Any /Exe available anywhere for your Exif compare prog? PinmvpNishant Sivakumar7-Oct-10 9:08 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? PinmemberWolfram Kuss2-Sep-11 0:46 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? PinmvpNishant Sivakumar2-Sep-11 1:30 
GeneralRe: Any /Exe available anywhere for your Exif compare prog? PinmemberWolfram Kuss2-Sep-11 21:24 
GeneralNice, Can't belive I missed this! PinmemberAlan Beasley14-Apr-10 8:00 
GeneralRe: Nice, Can't belive I missed this! PinmvpNishant Sivakumar14-Apr-10 8:08 
GeneralRe: Nice, Can't belive I missed this! PinmemberAlan Beasley14-Apr-10 8:31 
GeneralRe: Nice, Can't belive I missed this! PinmvpNishant Sivakumar14-Apr-10 8:36 
GeneralRe: Nice, Can't belive I missed this! PinmemberAlan Beasley14-Apr-10 8:54 
GeneralRe: Nice, Can't belive I missed this! PinmemberAlan Beasley14-Apr-10 9:23 
GeneralRe: Nice, Can't belive I missed this! PinmvpNishant Sivakumar14-Apr-10 9:36 
GeneralRe: Nice, Can't belive I missed this! PinmemberAlan Beasley14-Apr-10 9:59 
GeneralRe: Nice, Can't belive I missed this! PinmvpNishant Sivakumar14-Apr-10 10:01 
GeneralRe: Nice, Can't belive I missed this! PinmemberAlan Beasley14-Apr-10 10:17 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140827.1 | Last Updated 12 Apr 2010
Article Copyright 2010 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid