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

WPF: A Nice View BreadCrumb Manager

, 16 Mar 2010
Rate this:
Please Sign up or sign in to vote.
A re-usable breadcrumb control for WPF.

Table of Contents

Introduction

Some of you may know of Billy Hollis and his involvement with dnrTV, and a demo WPF app that he showcased to the general public some time ago. Basically, it showcased this pretty cool breadcrumb type control that allowed separate instances of WPF controls (known as Views) to be stored in a bread crumb like control. The user could then choose new Views which would be added to the bread crumb control, and the bread crumb control would show a count of the active items (basically anything in the breadcrumb control at that time was in memory), and also allowed the user to reload these active Views. The breadcrumb control as demonstrated by Billy Hollis also showcased a translating transition between the current bread crumb control view and the user selected new view.

Now, I really liked this, but since Billy Hollis was just showing a video of what he had done, there was no source code available. Which is a shame, as I have never seen anything like it until recently, where my home http://karlshifflett.wordpress.com released his BBQ Shack/Ocean 2 framework out there. Karl basically created a view service which he is calling "Non-Linear Application Navigation - View Manager Services", which you can read about at Karl's blog post.

So saying all that, what was left for little old me to do? Well, I did read through Karl's blog post, but I still felt I could have a go at creating my own re-usable breadcrumb control that allows users to quickly add this sort of ability to their own WPF applications.

I must confess that although I read Karl's blog post, I did not examine the code, so I am expecting there to be practically no overlap in what my code does. I have basically coded from scratch, and attempted to create a BreadCrumb control that does all the things I would expect a re-usable breadcrumb Control to do, and allow the user to use it with the minimum of fuss.

This article represents the work that I have done to create a highly re-usable breadcrumb control for WPF.

Prerequisites

I am pretty happy to say that all you need to run and use the attached code is Visual Studio 2008 or higher, and .NET 3.5 SP1 installed.

Demo Video

This article is best demonstrated with a video (note: there is no audio), but I will explain all the video working in detail within this article.

Simply click on this image, which will take you to a new page showing the video. But before you do, I urge you to read about the points to look out for in the video, as by looking out for these points, you will gain a better understanding of how the code associated with this article works.

These are some of the things that you should take note of whilst watching the video:

  • The BreadCrumb control stores recallable instances of Views that are added to the BreadCrumb control by the user navigating around various Views (from now on known as crumbs).
  • That for each crumb (View) that is added to the BreadCrumb control, there is also a quick link crumb entry added, to allow the user to very quickly navigate back to a previously viewed crumb.
  • That a live mini visual representation of each contained crumb is available within a popup for each type of crumb added. Where for each crumb within the BreadCrumb control, the user will see a current visual representation of the crumb. This allows the user to easily identify which of the BreadCrumb contained crumbs (Views) they may wish to go back and reload.
  • That the user may choose to view or remove a BreadCrumb held crumb (View).
  • That the BreadCrumb control has a concept of examining an "IsDirty" flag, which can be used to alert the user prior to removing a crumb from the BreadCrumb control.
  • That there are numerous types of transitions available within the BreadCrumb to switch from the active crumb (View) to the newly requested crumb (View), allowing the user to pick which type of transition they prefer.

If you missed all that, which I am sure you will the first time, I urge you to review the video as it will help you understand the rest of this article a bit better.

A Word About the Demo App

Before we dive into the code, I just wanted to discuss one small aspect of the attached demo code, and its structure.

When you load the attached code up in Visual Studio, you will see something like this:

You will note that there are three projects (C#, sorry VB'ers amongst you). The reason for this is that BreadCrumbControl is a re-usable DLL that is self contained, and has all code and WPF Styles/Templates/Converters etc. So if you don't like its visual style (in effect, mine, as I am its owner/creator, I gave birth to the monster), the BreadCrumbControl DLL is the place to start with some re-styling (should you want to). 

The second project BreadCrumbSystem is simply a throw away demo app that hosts a single instance of BreadCrumbControl and also contains two dummy crumbs that are simply used to showcase the capabilities of the BreadCrumbControl DLL.

Now, when I say throw away demo code, I do mean that, but there is some code in there that will set you on your way to using the BreadCrumbControl in your own projects, but I will get to that later. It's really easy actually, which has pleased me greatly; it's like 3-4 lines of window code, and like two properties for each View you want to crumb'ify, but more on that later.

Just so we are crystal clear about what is just demo code and what is the main thrust of this article, let's consider the following image:

The image above contains three areas that we need to consider:

  1. There is a hosting Window (WPFBreadCrumbSystem.Window1) which has buttons to add the two dummy Views to the hosted instance of BreadCrumbControl.BreadCrumbViewManager. The Window is shown here in purple, but obviously also includes the green area, which is the actual instance of the BreadCrumbControl. The Window is pretty much throw away; it does, however, show you what you need to do to get BreadCrumbControl.BreadCrumbViewManager to work for you in your own app. Do not worry, I will explain all that later on.
  2. The actual instance of the BreadCrumbControl, which is shown above in green, and as shown in this image, is currently displaying one of the demo app's crumbs/Views (basically, just made to showcase the BreadCrumbControl.BreadCrumbViewManager capabilities).
  3. A demo crumb/View, which is shown in the blue area above. Whilst these crumbs/Views are pretty much throw away, they do show you what you need to do to get the BreadCrumbControl.BreadCrumbViewManager to work for you in your own app. Do not worry, I will explain all that later on. The attached code contains two throw away demo crumbs/Views: ImageControl, MusicControl.

And the third project BreadCrumbSystemMVVM is to demonstrate the capabilities of the BreadCrumbControl DLL within an MVVM application. 

How Does it All Work

This section and its subsections will go through the inner workings of the BreadCrumbViewManager control presented in this article.

The Basic Idea

At its heart, there is a single control BreadCrumbViewManager that controls the showing/hiding of crumbs (Views) and maintains a breadcrumb trail, and also allows the users to pick between four available transitions. The user is free to use them to show previously visited crumbs. It is not a complicated control actually, and the main underlying thing that makes the BreadCrumbViewManager work is actually just a special ObservableDictionary.

The following steps outline how the BreadCrumbViewManager works:

  1. The BreadCrumbViewManager should be added to the VisualTree of some other Window/UserControl (I will tell you about this later on).
  2. The user can create their own crumbs, which are UserControls that have implemented the BreadCrumbControl.IBreadCrumbView, which is discussed right here.
  3. The user can then add crumbs to the BreadCrumbViewManager, and when a new crumb is added, a couple of things happen:
    • A check is done to make sure the crumb is not null.
    • A check is also done to see if there is already a key (within the dictionary, which we will discuss soon) for the Type of the crumb being added. If there is a key for the Type of the crumb being added, the new crumb is wrapped in a WrappedIBreadCrumbView and then added to an ObservableCollection<WrappedIBreadCrumbView> associated with the key for the current crumb Type. If no key exists for the current crumb Type, a new entry is added to the dictionary using the crumb Type and a new ObservableCollection<WrappedIBreadCrumbView> which contains a single item which is a WrappedIBreadCrumbView around the current crumb.

Believe it or not, that is almost all we need to do in order to get a crumb into the BreadCrumbViewManager; the rest is down to the magic of Binding.

Here is the entire code for the BreadCrumbViewManager; see, it really isn't that bad, is it?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Transitionals.Transitions;
using Transitionals;
using System.Collections.ObjectModel;
using System.Windows.Controls.Primitives;

namespace BreadCrumbControl
{
    public enum TransitionType
    {
        FadeAndGrow=1,
        Translate,
        FadeAndBlur,
        Rotate
    }

    /// <summary>
    /// Interaction logic for BreadCrumbViewManager.xaml
    /// </summary>
    public partial class BreadCrumbViewManager : UserControl
    {
        private TransitionType currentTransitionType = TransitionType.FadeAndGrow;
        private Dictionary<TransitionType, Transition> 
            transitionsMap = new Dictionary<TransitionType, Transition>();
        private new 
            ObservableDictionary<Type, ObservableCollection<WrappedIBreadCrumbView>> 
            crumbs = new ObservableDictionary<Type, 
            ObservableCollection<WrappedIBreadCrumbView>>();

        public BreadCrumbViewManager()
        {
            this.DataContext = crumbs;
            InitializeComponent();
            SetupTransitions();
        }

        public void AddCrumb(IBreadCrumbView newCrumb)
        {
            if (newCrumb != null)
            {
                Visual visual = newCrumb as Visual;
                if (visual != null)
                {
                    transitionBox.Content = newCrumb;
                    if (!crumbs.ContainsKey(newCrumb.GetType()))
                    {
                        ObservableCollection<WrappedIBreadCrumbView> localCrumbs =
                            new ObservableCollection<WrappedIBreadCrumbView>();

                        localCrumbs.Add(CreateWrapper(newCrumb));
                        crumbs.Add(newCrumb.GetType(), localCrumbs);
                    }
                    else
                    {
                        crumbs[newCrumb.GetType()].Add(CreateWrapper(newCrumb));
                    }
                }
            }
        }

        public void ApplyNewTransitionType(TransitionType newTransitionType)
        {
            try
            {
                transitionBox.Transition = transitionsMap[newTransitionType];
            }
            catch
            {
                transitionBox.Transition = 
                   transitionsMap[TransitionType.FadeAndGrow];
            }
            
        }

        private void SetupTransitions()
        {
            transitionsMap.Add(TransitionType.FadeAndBlur, 
                               new FadeAndBlurTransition());
            transitionsMap.Add(TransitionType.FadeAndGrow, 
                               new FadeAndGrowTransition());
            transitionsMap.Add(TransitionType.Translate, 
                               new TranslateTransition());
            transitionsMap.Add(TransitionType.Rotate, new RotateTransition());
            transitionBox.Transition = transitionsMap[TransitionType.FadeAndGrow];
        }

        private WrappedIBreadCrumbView CreateWrapper(IBreadCrumbView newCrumb)
        {
            WrappedIBreadCrumbView wrapper = new WrappedIBreadCrumbView();
            wrapper.BreadCrumbItem = newCrumb;
            wrapper.BreadCrumbItemAsBrush = new VisualBrush(newCrumb as Visual);
            return wrapper;
        }

        private void TransitionButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                Button button = sender as Button;
                if (button != null && button.Tag != null)
                {
                    String selectedTransitionType = button.Tag.ToString();
                    TransitionType newTransitionType = (TransitionType)Enum.Parse(
                        typeof(TransitionType), selectedTransitionType);
                    transitionBox.Transition = transitionsMap[newTransitionType];
                }
            }
            catch
            {
                transitionBox.Transition = transitionsMap[TransitionType.FadeAndGrow];
            }
        }

        private void RemoveCrumb_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                WrappedIBreadCrumbView crumbToRemove = 
                    (WrappedIBreadCrumbView)((Button)sender).Tag;

                IBreadCrumbView currentCrumbView =
                    (IBreadCrumbView)transitionBox.Content;

                IBreadCrumbView crumbToRemoveView = 
                    (IBreadCrumbView)crumbToRemove.BreadCrumbItem;

                if (crumbToRemoveView.IsDirty)
                {
                    if (MessageBox.Show(
                        "The current crumb is Dirty, Possible changes exist " +
                        "\r\nDo you really want to remove it",
                        "Confirm Remove", MessageBoxButton.YesNo, 
                        MessageBoxImage.Question) == MessageBoxResult.Yes)
                    {
                        CheckForCurrentCrumbAndConfirmRemoval(crumbToRemove, 
                            currentCrumbView, crumbToRemoveView);
                    }
                }
                else
                {
                    CheckForCurrentCrumbAndConfirmRemoval(crumbToRemove, 
                        currentCrumbView, crumbToRemoveView);
                }
            }
            catch
            {
                //not much we can do about it
            }

        }

        private void CheckForCurrentCrumbAndConfirmRemoval(
            WrappedIBreadCrumbView crumbToRemove, 
            IBreadCrumbView currentCrumbView, 
            IBreadCrumbView crumbToRemoveView)
        {
            if (Object.ReferenceEquals(currentCrumbView, crumbToRemoveView))
            {
                if (MessageBox.Show("You are attempting " + 
                        "to remove the current item\r\nPlease confirm",
                    "Confirm Remove", MessageBoxButton.YesNo, 
                    MessageBoxImage.Question) == MessageBoxResult.Yes)
                {
                    RemoveCrumb(crumbToRemove);
                }
            }
            else
            {
                RemoveCrumb(crumbToRemove);
            }
        }

        private void RemoveCrumb(WrappedIBreadCrumbView crumbToRemove)
        {
            Type crumbType = crumbToRemove.BreadCrumbItem.GetType();
            crumbs[crumbType].Remove(crumbToRemove);
            if (crumbs[crumbType].Count == 0)
                crumbs.Remove(crumbType);
        }

        private void HidePopup_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                Popup popup = (Popup)((Button)sender).Tag;
                popup.IsOpen = false;
            }
            catch
            {
            }
        }
     
        private void ViewCrumb_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                WrappedIBreadCrumbView crumbToView = 
                    (WrappedIBreadCrumbView)((Button)sender).Tag;
                Type crumbType = crumbToView.BreadCrumbItem.GetType();
                transitionBox.Content = crumbToView.BreadCrumbItem;
            }
            catch
            {
                //not much we can do about it
            }
        } 
    }
}

Obviously, to make all this magic happen, there are a number of classes, and a fair chunk of XAML, but one of the main classes (apart from the BreadCrumbViewManager) that makes it all possible is the ObservableDictionary, which allows WPF's DataBinding to bind to a Key/Value pair. Now, I can not take credit for this class, it actually comes from the dark recesses of Dr WPF's mind.

It's a very nice class, and here it is:

/* Copyright (c) 2007, Dr. WPF
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 
 *   * The name Dr. WPF may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY Dr. WPF ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Dr. WPF BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Runtime.InteropServices;

namespace BreadCrumbControl
{
    [Serializable]
    public class ObservableDictionary<TKey, TValue> :
        IDictionary<TKey, TValue>,
        ICollection<KeyValuePair<TKey, TValue>>,
        IEnumerable<KeyValuePair<TKey, TValue>>,
        IDictionary,
        ICollection,
        IEnumerable,
        ISerializable,
        IDeserializationCallback,
        INotifyCollectionChanged,
        INotifyPropertyChanged
    {
        #region constructors

        #region public

        public ObservableDictionary()
        {
            _keyedEntryCollection = 
              new KeyedDictionaryEntryCollection<TKey>();
        }

        public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
        {
            _keyedEntryCollection = new KeyedDictionaryEntryCollection<TKey>();

            foreach (KeyValuePair<TKey, TValue> entry in dictionary)
                DoAddEntry((TKey)entry.Key, (TValue)entry.Value);
        }

        public ObservableDictionary(IEqualityComparer<TKey> comparer)
        {
            _keyedEntryCollection = 
                new KeyedDictionaryEntryCollection<TKey>(comparer);
        }

        public ObservableDictionary(IDictionary<TKey, TValue> dictionary, 
            IEqualityComparer<TKey> comparer)
        {
            _keyedEntryCollection = 
               new KeyedDictionaryEntryCollection<TKey>(comparer);

            foreach (KeyValuePair<TKey, TValue> entry in dictionary)
                DoAddEntry((TKey)entry.Key, (TValue)entry.Value);
        }

        #endregion public

        #region protected

        protected ObservableDictionary(SerializationInfo info, 
            StreamingContext context)
        {
            _siInfo = info;
        }

        #endregion protected

        #endregion constructors

        #region properties

        #region public

        public IEqualityComparer<TKey> Comparer
        {
            get { return _keyedEntryCollection.Comparer; }
        }

        public int Count
        {
            get { return _keyedEntryCollection.Count; }
        }

        public Dictionary<TKey, TValue>.KeyCollection Keys
        {
            get { return TrueDictionary.Keys; }
        }

        public TValue this[TKey key]
        {
            get { return (TValue)_keyedEntryCollection[key].Value; }
            set { DoSetEntry(key, value); }
        }

        public Dictionary<TKey, TValue>.ValueCollection Values
        {
            get { return TrueDictionary.Values; }
        }

        #endregion public

        #region private

        private Dictionary<TKey, TValue> TrueDictionary
        {
            get
            {
                if (_dictionaryCacheVersion != _version)
                {
                    _dictionaryCache.Clear();
                    foreach (DictionaryEntry entry in _keyedEntryCollection)
                        _dictionaryCache.Add((TKey)entry.Key, (TValue)entry.Value);
                    _dictionaryCacheVersion = _version;
                }
                return _dictionaryCache;
            }
        }

        #endregion private

        #endregion properties

        #region methods

        #region public

        public void Add(TKey key, TValue value)
        {
            DoAddEntry(key, value);
        }

        public void Clear()
        {
            DoClearEntries();
        }

        public bool ContainsKey(TKey key)
        {
            return _keyedEntryCollection.Contains(key);
        }

        public bool ContainsValue(TValue value)
        {
            return TrueDictionary.ContainsValue(value);
        }

        public IEnumerator GetEnumerator()
        {
            return new Enumerator<TKey, TValue>(this, false);
        }

        public bool Remove(TKey key)
        {
            return DoRemoveEntry(key);
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            bool result = _keyedEntryCollection.Contains(key);
            value = result ? 
                (TValue)_keyedEntryCollection[key].Value : default(TValue);
            return result;
        }

        #endregion public

        #region protected

        protected virtual bool AddEntry(TKey key, TValue value)
        {
            _keyedEntryCollection.Add(new DictionaryEntry(key, value));
            return true;
        }

        protected virtual bool ClearEntries()
        {
            // check whether there are entries to clear
            bool result = (Count > 0);
            if (result)
            {
                // if so, clear the dictionary
                _keyedEntryCollection.Clear();
            }
            return result;
        }

        protected int GetIndexAndEntryForKey(TKey key, 
                         out DictionaryEntry entry)
        {
            entry = new DictionaryEntry();
            int index = -1;
            if (_keyedEntryCollection.Contains(key))
            {
                entry = _keyedEntryCollection[key];
                index = _keyedEntryCollection.IndexOf(entry);
            }
            return index;
        }

        protected virtual void OnCollectionChanged(
            NotifyCollectionChangedEventArgs args)
        {
            if (CollectionChanged != null)
                CollectionChanged(this, args);
        }

        protected virtual void OnPropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, 
                    new PropertyChangedEventArgs(name));
        }

        protected virtual bool RemoveEntry(TKey key)
        {
            // remove the entry
            return _keyedEntryCollection.Remove(key);
        }

        protected virtual bool SetEntry(TKey key, TValue value)
        {
            bool keyExists = _keyedEntryCollection.Contains(key);

            // if identical key/value pair already exists, nothing to do
            if (keyExists && 
                value.Equals((TValue)_keyedEntryCollection[key].Value))
                return false;

            // otherwise, remove the existing entry
            if (keyExists)
                _keyedEntryCollection.Remove(key);

            // add the new entry
            _keyedEntryCollection.Add(new DictionaryEntry(key, value));

            return true;
        }

        #endregion protected

        #region private

        private void DoAddEntry(TKey key, TValue value)
        {
            if (AddEntry(key, value))
            {
                _version++;

                DictionaryEntry entry;
                int index = GetIndexAndEntryForKey(key, out entry);
                FireEntryAddedNotifications(entry, index);
            }
        }

        private void DoClearEntries()
        {
            if (ClearEntries())
            {
                _version++;
                FireResetNotifications();
            }
        }

        private bool DoRemoveEntry(TKey key)
        {
            DictionaryEntry entry;
            int index = GetIndexAndEntryForKey(key, out entry);

            bool result = RemoveEntry(key);
            if (result)
            {
                _version++;
                if (index > -1)
                    FireEntryRemovedNotifications(entry, index);
            }

            return result;
        }

        private void DoSetEntry(TKey key, TValue value)
        {
            DictionaryEntry entry;
            int index = GetIndexAndEntryForKey(key, out entry);

            if (SetEntry(key, value))
            {
                _version++;

                // if prior entry existed for this key,
                // fire the removed notifications
                if (index > -1)
                    FireEntryRemovedNotifications(entry, index);

                // then fire the added notifications
                index = GetIndexAndEntryForKey(key, out entry);
                FireEntryAddedNotifications(entry, index);
            }
        }

        private void FireEntryAddedNotifications(DictionaryEntry entry, int index)
        {
            // fire the relevant PropertyChanged notifications
            FirePropertyChangedNotifications();

            // fire CollectionChanged notification
            if (index > -1)
                OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Add, 
                        new KeyValuePair<TKey, TValue>((TKey)entry.Key, 
                                   (TValue)entry.Value), index));
            else
                OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Reset));
        }

        private void FireEntryRemovedNotifications(DictionaryEntry entry, int index)
        {
            // fire the relevant PropertyChanged notifications
            FirePropertyChangedNotifications();

            // fire CollectionChanged notification
            if (index > -1)
                OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Remove, 
                        new KeyValuePair<TKey, TValue>((TKey)entry.Key, 
                           (TValue)entry.Value), index));
            else
                OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                    NotifyCollectionChangedAction.Reset));
        }

        private void FirePropertyChangedNotifications()
        {
            if (Count != _countCache)
            {
                _countCache = Count;
                OnPropertyChanged("Count");
                OnPropertyChanged("Item[]");
                OnPropertyChanged("Keys");
                OnPropertyChanged("Values");
            }
        }

        private void FireResetNotifications()
        {
            // fire the relevant PropertyChanged notifications
            FirePropertyChangedNotifications();

            // fire CollectionChanged notification
            OnCollectionChanged(
                new NotifyCollectionChangedEventArgs(
                NotifyCollectionChangedAction.Reset));
        }

        #endregion private

        #endregion methods

        #region interfaces

        #region IDictionary<TKey, TValue>

        void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
        {
            DoAddEntry(key, value);
        }

        bool IDictionary<TKey, TValue>.Remove(TKey key)
        {
            return DoRemoveEntry(key);
        }

        bool IDictionary<TKey, TValue>.ContainsKey(TKey key)
        {
            return _keyedEntryCollection.Contains(key);
        }

        bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value)
        {
            return TryGetValue(key, out value);
        }

        ICollection<TKey> IDictionary<TKey, TValue>.Keys
        {
            get { return Keys; }
        }

        ICollection<TValue> IDictionary<TKey, TValue>.Values
        {
            get { return Values; }
        }

        TValue IDictionary<TKey, TValue>.this[TKey key]
        {
            get { return (TValue)_keyedEntryCollection[key].Value; }
            set { DoSetEntry(key, value); }
        }

        #endregion IDictionary<TKey, TValue>

        #region IDictionary

        void IDictionary.Add(object key, object value)
        {
            DoAddEntry((TKey)key, (TValue)value);
        }

        void IDictionary.Clear()
        {
            DoClearEntries();
        }

        bool IDictionary.Contains(object key)
        {
            return _keyedEntryCollection.Contains((TKey)key);
        }

        IDictionaryEnumerator IDictionary.GetEnumerator()
        {
            return new Enumerator<TKey, TValue>(this, true);
        }

        bool IDictionary.IsFixedSize
        {
            get { return false; }
        }

        bool IDictionary.IsReadOnly
        {
            get { return false; }
        }

        object IDictionary.this[object key]
        {
            get { return ((IDictionary)this)[(TKey)key]; }
            set { DoSetEntry((TKey)key, (TValue)value); }
        }

        ICollection IDictionary.Keys
        {
            get { return Keys; }
        }

        void IDictionary.Remove(object key)
        {
            DoRemoveEntry((TKey)key);
        }

        ICollection IDictionary.Values
        {
            get { return Values; }
        }

        #endregion IDictionary

        #region ICollection<KeyValuePair<TKey, TValue>>

        void ICollection<KeyValuePair<TKey, TValue>>.Add(
                         KeyValuePair<TKey, TValue> kvp)
        {
            DoAddEntry(kvp.Key, kvp.Value);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Clear()
        {
            DoClearEntries();
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Contains(
            KeyValuePair<TKey, TValue> kvp)
        {
            return _keyedEntryCollection.Contains(kvp.Key);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(
            KeyValuePair<TKey, TValue>[] array, int index)
        {
            if (array == null)
            {
                throw new ArgumentNullException(
                    "CopyTo() failed:  array parameter was null");
            }
            if ((index < 0) || (index > array.Length))
            {
                throw new ArgumentOutOfRangeException(
                    "CopyTo() failed:  index parameter was " + 
                    "outside the bounds of the supplied array");
            }
            if ((array.Length - index) < _keyedEntryCollection.Count)
            {
                throw new ArgumentException("CopyTo() " + 
                      "failed:  supplied array was too small");
            }

            foreach (DictionaryEntry entry in _keyedEntryCollection)
                array[index++] = 
                    new KeyValuePair<TKey, TValue>(
                    (TKey)entry.Key, (TValue)entry.Value);
        }

        int ICollection<KeyValuePair<TKey, TValue>>.Count
        {
            get { return _keyedEntryCollection.Count; }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
        {
            get { return false; }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(
                         KeyValuePair<TKey, TValue> kvp)
        {
            return DoRemoveEntry(kvp.Key);
        }

        #endregion ICollection<KeyValuePair<TKey, TValue>>

        #region ICollection

        void ICollection.CopyTo(Array array, int index)
        {
            ((ICollection)_keyedEntryCollection).CopyTo(array, index);
        }

        int ICollection.Count
        {
            get { return _keyedEntryCollection.Count; }
        }

        bool ICollection.IsSynchronized
        {
            get { return ((ICollection)_keyedEntryCollection).IsSynchronized; }
        }

        object ICollection.SyncRoot
        {
            get { return ((ICollection)_keyedEntryCollection).SyncRoot; }
        }

        #endregion ICollection

        #region IEnumerable<KeyValuePair<TKey, TValue>>

        IEnumerator<KeyValuePair<TKey, TValue>> 
            IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
        {
            return new Enumerator<TKey, TValue>(this, false);
        }

        #endregion IEnumerable<KeyValuePair<TKey, TValue>>

        #region IEnumerable

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion IEnumerable

        #region ISerializable

        public virtual void GetObjectData(SerializationInfo info, 
            StreamingContext context)
        {
            if (info == null)
            {
                throw new ArgumentNullException("info");
            }

            Collection<DictionaryEntry> entries = 
                          new Collection<DictionaryEntry>();
            foreach (DictionaryEntry entry in _keyedEntryCollection)
                entries.Add(entry);
            info.AddValue("entries", entries);
        }

        #endregion ISerializable

        #region IDeserializationCallback

        public virtual void OnDeserialization(object sender)
        {
            if (_siInfo != null)
            {
                Collection<DictionaryEntry> entries = (Collection<DictionaryEntry>)
                    _siInfo.GetValue("entries", typeof(Collection<DictionaryEntry>));
                foreach (DictionaryEntry entry in entries)
                    AddEntry((TKey)entry.Key, (TValue)entry.Value);
            }
        }

        #endregion IDeserializationCallback

        #region INotifyCollectionChanged

        event NotifyCollectionChangedEventHandler 
            INotifyCollectionChanged.CollectionChanged
        {
            add { CollectionChanged += value; }
            remove { CollectionChanged -= value; }
        }

        protected virtual event 
          NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion INotifyCollectionChanged

        #region INotifyPropertyChanged

        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add { PropertyChanged += value; }
            remove { PropertyChanged -= value; }
        }

        protected virtual event PropertyChangedEventHandler PropertyChanged;

        #endregion INotifyPropertyChanged

        #endregion interfaces

        #region protected classes

        #region KeyedDictionaryEntryCollection<TKey>

        protected class KeyedDictionaryEntryCollection<TKey> : 
            KeyedCollection<TKey, DictionaryEntry>
        {
            #region constructors

            #region public

            public KeyedDictionaryEntryCollection() : base() { }

            public KeyedDictionaryEntryCollection(IEqualityComparer<TKey> comparer) 
                : base(comparer) { }

            #endregion public

            #endregion constructors

            #region methods

            #region protected

            protected override TKey GetKeyForItem(DictionaryEntry entry)
            {
                return (TKey)entry.Key;
            }

            #endregion protected

            #endregion methods
        }

        #endregion KeyedDictionaryEntryCollection<TKey>

        #endregion protected classes

        #region public structures

        #region Enumerator

        [Serializable, StructLayout(LayoutKind.Sequential)]
        public struct Enumerator<TKey, TValue> 
            : IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
            IDictionaryEnumerator, IEnumerator
        {
            #region constructors

            internal Enumerator(ObservableDictionary<TKey, TValue> dictionary, 
                bool isDictionaryEntryEnumerator)
            {
                _dictionary = dictionary;
                _version = dictionary._version;
                _index = -1;
                _isDictionaryEntryEnumerator = isDictionaryEntryEnumerator;
                _current = new KeyValuePair<TKey, TValue>();
            }

            #endregion constructors

            #region properties

            #region public

            public KeyValuePair<TKey, TValue> Current
            {
                get
                {
                    ValidateCurrent();
                    return _current;
                }
            }

            #endregion public

            #endregion properties

            #region methods

            #region public

            public void Dispose()
            {
            }

            public bool MoveNext()
            {
                ValidateVersion();
                _index++;
                if (_index < _dictionary._keyedEntryCollection.Count)
                {
                    _current = new KeyValuePair<TKey, TValue>
                        ((TKey)_dictionary._keyedEntryCollection[_index].Key, 
                        (TValue)_dictionary._keyedEntryCollection[_index].Value);
                    return true;
                }
                _index = -2;
                _current = new KeyValuePair<TKey, TValue>();
                return false;
            }

            #endregion public

            #region private

            private void ValidateCurrent()
            {
                if (_index == -1)
                {
                    throw new InvalidOperationException(
                        "The enumerator has not been started.");
                }
                else if (_index == -2)
                {
                    throw new InvalidOperationException(
                        "The enumerator has reached " + 
                        "the end of the collection.");
                }
            }

            private void ValidateVersion()
            {
                if (_version != _dictionary._version)
                {
                    throw new InvalidOperationException(
                        "The enumerator is not valid" + 
                        " because the dictionary changed.");
                }
            }

            #endregion private

            #endregion methods

            #region IEnumerator implementation

            object IEnumerator.Current
            {
                get
                {
                    ValidateCurrent();
                    if (_isDictionaryEntryEnumerator)
                    {
                        return new DictionaryEntry(_current.Key, 
                                                   _current.Value);
                    }
                    return new KeyValuePair<TKey, TValue>(_current.Key, 
                                                          _current.Value);
                }
            }

            void IEnumerator.Reset()
            {
                ValidateVersion();
                _index = -1;
                _current = new KeyValuePair<TKey, TValue>();
            }

            #endregion IEnumerator implemenation

            #region IDictionaryEnumerator implemenation

            DictionaryEntry IDictionaryEnumerator.Entry
            {
                get
                {
                    ValidateCurrent();
                    return new DictionaryEntry(_current.Key, _current.Value);
                }
            }
            object IDictionaryEnumerator.Key
            {
                get
                {
                    ValidateCurrent();
                    return _current.Key;
                }
            }
            object IDictionaryEnumerator.Value
            {
                get
                {
                    ValidateCurrent();
                    return _current.Value;
                }
            }

            #endregion

            #region fields

            private ObservableDictionary<TKey, TValue> _dictionary;
            private int _version;
            private int _index;
            private KeyValuePair<TKey, TValue> _current;
            private bool _isDictionaryEntryEnumerator;

            #endregion fields
        }

        #endregion Enumerator

        #endregion public structures

        #region fields

        protected KeyedDictionaryEntryCollection<TKey> _keyedEntryCollection;

        private int _countCache = 0;
        private Dictionary<TKey, TValue> _dictionaryCache 
		= new Dictionary<TKey, TValue>();
        private int _dictionaryCacheVersion = 0;
        private int _version = 0;

        [NonSerialized]
        private SerializationInfo _siInfo = null;

        #endregion fields
    }
}

So how does this ObservableDictionary get used in the BreadCrumbViewManager XAML? Well, the nearly whole BreadCrumbViewManager XAML looks like this:

<UserControl x:Class="BreadCrumbControl.BreadCrumbViewManager"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BreadCrumbControl"
    xmlns:transitionals=
      "clr-namespace:Transitionals;assembly=Transitionals"
    xmlns:transitionalsControls=
      "clr-namespace:Transitionals.Controls;assembly=Transitionals"
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Stretch">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary 
                      Source="../Resources/AppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>


        <Expander ExpandDirection="Down" Margin="0" 
               Grid.Row="0" IsExpanded="False"
               Style="{StaticResource ExpanderStyle1}">
            <Grid HorizontalAlignment="Stretch" 
                      Height="Auto" Background="Black">
                <StackPanel Orientation="Horizontal" Background="Black" 
                            HorizontalAlignment="Right" Height="Auto">
                    <Label Content="Pick Transition" FontFamily="Verdana" 
                            FontSize="10" VerticalContentAlignment="Center" 
                            VerticalAlignment="Center"
                       Foreground="LightGray"/>
                    <Button  Width="24" Height="24" 
                             ToolTip="Fade And Grow" 
                             Margin="3" Tag="FadeAndGrow"
                             Style="{StaticResource transitonButtonStyle}" 
                             Click="TransitionButton_Click">
                        <Image Source="../Images/grow.png" 
                           VerticalAlignment="Center" 
                           HorizontalAlignment="Center" 
                           Width="16" Height="16"/>
                    </Button>

                    <Button  Width="24" 
                             Height="24" ToolTip="Fade And Blur" 
                             Margin="3" Tag="FadeAndBlur" 
                             Style="{StaticResource transitonButtonStyle}" 
                             Click="TransitionButton_Click">
                        <Image Source="../Images/grow.png" 
                           VerticalAlignment="Center" 
                           HorizontalAlignment="Center" 
                           Width="16" Height="16"/>
                    </Button>

                    <Button  Width="24" Height="24" ToolTip="Translate" 
                             Margin="3" Tag="Translate"  
                             Style="{StaticResource transitonButtonStyle}" 
                             Click="TransitionButton_Click">
                        <Image Source="../Images/move.png" VerticalAlignment="Center" 
                           HorizontalAlignment="Center" Width="16" Height="16"/>
                    </Button>

                    <Button  Width="24" Height="24" ToolTip="Rotate" 
                             Margin="3" Tag="Rotate"  
                             Style="{StaticResource transitonButtonStyle}" 
                             Click="TransitionButton_Click">
                        <Image Source="../Images/rotate.png" VerticalAlignment="Center" 
                           HorizontalAlignment="Center" Width="16" Height="16"/>
                    </Button>

                </StackPanel>

            </Grid>
        </Expander>

        <transitionalsControls:TransitionElement Grid.Row="1"
            x:Name="transitionBox" 
            Transition="{Binding}" />

        <local:FrictionScrollViewer x:Name="ScrollViewer" Grid.Row="2"
                      Style="{StaticResource ScrollViewerStyle}">

            <ItemsControl x:Name="items" ItemsSource="{Binding}">

                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel
                            IsItemsHost="True"
                            Orientation="Horizontal"  />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>

                <ItemsControl.ItemTemplate>
                    <DataTemplate>

                        <ToggleButton x:Name="btn" 
                                Style="{StaticResource crumbButtonStyle}" 
                                Margin="15,5,15,5" 
                                ToolTip="{Binding Value[0].BreadCrumbItem.DisplayName}">

                            <Grid>

                                <Label Content="{Binding Value.Count}"
                                       Margin="0,0,-40,0" 
                                       VerticalAlignment="Bottom" 
                                       HorizontalAlignment="Right"
                                       FontWeight="Bold" 
                                       FontSize="16"
                                       VerticalContentAlignment="Center"
                                       HorizontalContentAlignment="Center"
                                       Foreground="Orange"/>

                                <ContentPresenter Width="Auto" Height="Auto"
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center"
                                    Content="{Binding Value[0].BreadCrumbItem, 
                                    Converter={StaticResource crumbImageConv}}"/>

                                <Popup x:Name="pop"  
                                       Placement="RelativePoint"
                                       VerticalOffset="-25"
                                       HorizontalOffset="0"
                                       IsOpen="{Binding ElementName=btn,Path=IsChecked}"
                                       Width="200" Height="200"
                                       AllowsTransparency="True"
                                       StaysOpen="true"
                                       PopupAnimation="Scroll">
				
				<!-- Will explain this in the Live Preview section -->
				<!-- Will explain this in the Live Preview section -->
				<!-- Will explain this in the Live Preview section -->
				<!-- Will explain this in the Live Preview section -->
				<!-- Will explain this in the Live Preview section -->
				<!-- Will explain this in the Live Preview section -->

                                </Popup>

                            </Grid>

                        </ToggleButton>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>

            </ItemsControl>

        </local:FrictionScrollViewer>

    </Grid>
</UserControl>

To make sense of this a bit more, consider the following image:

It can be seen from this image that the BreadCrumbViewManager is made up of a number of separate areas: you have the Expander area, where the user can pick the current transition to use. Then there is the transition container, which holds the active crumb and transitions to the newly requested crumb. Then there is the bottom area which is a friction enabled ScrollViwer (see FrictionScrollViewer), which I have used on a number of projects.

The FrictionScrollViewer holds an ItemsControl which is bound to the entire DataContext of the BreadCrumbViewManager. No prizes for guessing what that DataContext object may be... That's right, it's the ObservableDictionary. Within the ItemsControl, the ItemTemplate.DataTemplate is a ToggleButon. We will see more of how the ToggleButtons ControlTemplate is created later on.

What Makes a Crumb

So what exactly is a crumb then?

Well, put simple, a crumb can be any UserControl that you wish to allow the users to view or reload for another viewing. So that is all fine and dandy, but how do we make our typical Views (normally UserControl instances) BreadCrumb ready?

Well, it could not be easier, all we have to do is implement a very easy interface. The BreadCrumbControl.IBreadCrumbView, which is defined as follows:

using System;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

namespace BreadCrumbControl
{
    public interface IBreadCrumbView
    {
        BitmapImage CrumbImageUrl { get; }
        String DisplayName { get; }
        Boolean IsDirty { get; }
    }
}

I will just briefly discuss the different parts of this interface, as at the end of the article, I do a full run through of what you need to do to use the BreadCrumbControl in your own applications.

So starting at the top:

  • CrumbImageUrl: Is an instance of a BitmapImage class. This should contain a pointer to an image file that the BreadCrumbControl can use to show an Image for the type of crumb being added. As the BreadCrumbViewManager control only shows a single image for a type of crumb, there are certain optimizations that can be made to ensure that we only ever create a single instance of a BitmapImage class to satisfy the BreadCrumbControl.IBreadCrumbView interface.
  • DisplayName: Is a simple text string to represent the type of crumb, so something like "Simple Image View" would be totally fine.
  • IsDirty: Is a property that you can set within your View (crumb) that you may set when something changes. This flag is then checked inside the BreadCrumbViewManager to determine if the user should be asked a question before closing the crumb that has been requested to close. After all, if they changed something, they may have to go back and save the state, before closing the View.

It should be noted that when crumbs are added to the BreadCrumbViewManager control, they are wrapped in a new object called a WrappedIBreadCrumbView, which simply provides a few more bindable properties to the BreadCrumbViewManager control. I will talk more about this in the next section.

Live Preview

I think one of the most compelling features of this control is that you do actually get a live preview thumbnail of changes made to a crumb (View). Don't believe me, go and check out the Demo Video again and look out for the changes to the thumbnails for the crumb. I say, that's pretty cool.

So how is it achieved? Well, there are actually two parts to it.

Part 1: VisualBrush

The first part is surprisingly simple, we just create a WrappedIBreadCrumbView when we add a new IBreadCrumbView to the BreadCrumbViewManager, where the WrappedIBreadCrumbView looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Media;

namespace BreadCrumbControl
{
    public class WrappedIBreadCrumbView : INotifyPropertyChanged
    {
        private IBreadCrumbView breadCrumbItem;
        private VisualBrush breadCrumbItemAsBrush;

        public IBreadCrumbView BreadCrumbItem
        {
            get { return breadCrumbItem; }
            set
            {
                breadCrumbItem = value;
                NotifyChanged("BreadCrumbItem");
            }
        }

        public VisualBrush BreadCrumbItemAsBrush
        {
            get { return breadCrumbItemAsBrush; }
            set
            {
                breadCrumbItemAsBrush = value;
                NotifyChanged("BreadCrumbItemAsBrush");
            }
        }

        #region INotifyPropertyChanged Implementation

        /// <summary>
        /// Occurs when any properties are changed on this object.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;


        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param name="propertyNames">The names of the properties that changed.</param>
        protected virtual void NotifyChanged(params string[] propertyNames)
        {
            foreach (string name in propertyNames)
            {
                OnPropertyChanged(new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, e);
            }
        }

        #endregion
    }
}

Note the VisualBrush in there that makes a standard WPF VisualBrush of the newly added crumb (View). 

And this is how a WrappedIBreadCrumbView is created for a newly added crumb within the BreadCrumbViewManager:

public void AddCrumb(IBreadCrumbView newCrumb)
{
    if (newCrumb != null)
    {
        Visual visual = newCrumb as Visual;
        if (visual != null)
        {
            transitionBox.Content = newCrumb;
            if (!crumbs.ContainsKey(newCrumb.GetType()))
            {
                ObservableCollection<WrappedIBreadCrumbView> localCrumbs =
                    new ObservableCollection<WrappedIBreadCrumbView>();

                localCrumbs.Add(CreateWrapper(newCrumb));
                crumbs.Add(newCrumb.GetType(), localCrumbs);
            }
            else
            {
                crumbs[newCrumb.GetType()].Add(CreateWrapper(newCrumb));
            }
        }
    }
}

Part 2: A Fancy Button Template 

What use is a VisualBrush if you do not have anywhere to show it? So we need somewhere to show it, right? Luckily, I have thought of that, and have provided a ControlTemplate for a ToggleButton that shows all the historical crumbs that have been viewed for a particular type of View (basically, the key into the ObservableDictionary I was banging on about earlier).

Here is that ControlTemplate for a ToggleButton, where this is part of an ItemsControl.ItemTemplate, so happens for each and every unique Type of View (crumb) added to the BreadCrumbViewManager.

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <ToggleButton x:Name="btn" 
                Style="{StaticResource crumbButtonStyle}" 
                Margin="15,5,15,5" 
                ToolTip="{Binding Value[0].BreadCrumbItem.DisplayName}">

            <Grid>

                <Label Content="{Binding Value.Count}"
                       Margin="0,0,-40,0" 
                       VerticalAlignment="Bottom" 
                       HorizontalAlignment="Right"
                       FontWeight="Bold" 
                       FontSize="16"
                       VerticalContentAlignment="Center"
                       HorizontalContentAlignment="Center"
                       Foreground="Orange"/>

                <ContentPresenter Width="Auto" Height="Auto"
                    HorizontalAlignment="Center" 
                    VerticalAlignment="Center"
                    Content="{Binding Value[0].BreadCrumbItem, 
                    Converter={StaticResource crumbImageConv}}"/>
                
                <Popup x:Name="pop"  
                       Placement="RelativePoint"
                       VerticalOffset="-25"
                       HorizontalOffset="0"
                       IsOpen="{Binding ElementName=btn,Path=IsChecked}"
                       Width="200" Height="200"
                       AllowsTransparency="True"
                       StaysOpen="true"
                       PopupAnimation="Scroll">

                    <Border Background="Black" 
                                HorizontalAlignment="Stretch"
                                VerticalAlignment="Stretch"
                                BorderBrush="LightGray" 
                                BorderThickness="3" 
                                CornerRadius="5,5,5,5">

                        <Grid Background="Black">

                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="*"/>
                            </Grid.RowDefinitions>

                            <Thumb Grid.Row="0" 
                                   Width="Auto" Height="40" 
                                   Tag="{Binding ElementName=pop}"
                                   local:PopupBehaviours.IsMoveEnabledProperty="true">
                                <Thumb.Template>
                                    <ControlTemplate>
                                        <Border  Width="Auto" 
                                              Height="40" BorderBrush="#FF000000" 
                                              Background="LightGray" 
                                              VerticalAlignment="Top" 
                                              CornerRadius="5,5,0,0" 
                                              Margin="-2,-2,-2,0">
                                            
                                            <Grid HorizontalAlignment="Stretch" 
                                                     VerticalAlignment="Stretch">
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="*"/>
                                                    <ColumnDefinition Width="Auto"/>
                                                </Grid.ColumnDefinitions>


                                                <StackPanel Grid.Column="0"
                                                        Orientation="Horizontal" 
                                                        HorizontalAlignment="Stretch" 
                                                        VerticalAlignment="Center">

                                                    <Label Content="("
                                                       FontSize="18"
                                                       FontWeight="Bold"
                                                       Foreground="Black"
                                                       VerticalContentAlignment="Center"
                                                       Margin="5,0,0,0" />


                                                    <Label Content="{Binding Value.Count}"
                                                       FontSize="18"
                                                       FontWeight="Bold"
                                                       Foreground="Black"
                                                       VerticalContentAlignment="Center"
                                                       Margin="0,0,0,0" />

                                                    <Label Content=") Crumbs"
                                                       FontSize="18"
                                                       FontWeight="Bold"
                                                       Foreground="Black"
                                                       VerticalContentAlignment="Center"
                                                       Margin="0,0,0,0" />

                                                </StackPanel>

                                                <Button Width="40" Height="30" 
                                                        Grid.Column="1" 
                                                        Style="{StaticResource 
                                                               crumbControlButtonStyle}"
                                                        Tag="{Binding ElementName=pop}" 
                                                        Margin="5"
                                                        ToolTip="View Crumb" 
                                                        Click="HidePopup_Click" 
                                                        VerticalAlignment="Center" 
                                                        HorizontalAlignment="Left">
                                                    <Image Source="../Images/close.png" 
                                                       Width="22" Height="22"
                                                       HorizontalAlignment="Center" 
                                                       VerticalAlignment="Center"/>
                                                </Button>

                                            </Grid>
                                            
                                        </Border>
                                    </ControlTemplate>
                                </Thumb.Template>
                            </Thumb>

                            <local:FrictionScrollViewer 
                                   Background="Black" Grid.Row="1"
                                   Style="{StaticResource ScrollViewerStyle}" 
                                   Margin="0">

                                <ItemsControl x:Name="items" 
                                      Margin="0" AlternationCount="2"
                                      ItemsSource="{Binding Value}">

                                    <ItemsControl.ItemsPanel>
                                        <ItemsPanelTemplate>
                                            <StackPanel Orientation="Vertical" 
                                                Width="Auto" Height="Auto"
                                                IsItemsHost="True" />
                                        </ItemsPanelTemplate>
                                    </ItemsControl.ItemsPanel>

                                    <ItemsControl.ItemTemplate>
                                        <DataTemplate>

                                            <Grid x:Name="grid" Background="Black" 
                                                  HorizontalAlignment="Stretch" 
                                                  VerticalAlignment="Stretch">
                                                
                                                <Grid.RowDefinitions>
                                                    <RowDefinition Height="*"/>
                                                    <RowDefinition Height="*"/>
                                                </Grid.RowDefinitions>
                                                
                                                
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="Auto"/>
                                                    <ColumnDefinition Width="Auto"/>
                                                    <ColumnDefinition Width="*"/>
                                                </Grid.ColumnDefinitions>

                                          
                                                <Border x:Name="imgBord" Background="Black" 
                                                        Grid.Row="0" 
                                                        Grid.RowSpan="2" Grid.Column="0" 
                                                        Margin="3" 
                                                        CornerRadius="5" 
                                                        Width="Auto" Height="Auto" 
                                                        VerticalAlignment="Center">
                                                    <Rectangle 
                                                           VerticalAlignment="Center" 
                                                           HorizontalAlignment="Left"
                                                           Fill="{Binding 
                                                                 BreadCrumbItemAsBrush}"
                                                           Width="130" Height="65" 
                                                           Margin="3">
                                                        <Rectangle.ToolTip>
                                                            <Rectangle 
                                                              Fill="{Binding 
                                                                    BreadCrumbItemAsBrush}" 
                                                              Width="450" 
                                                              Height="225" Margin="2"/>
                                                        </Rectangle.ToolTip>
                                                    </Rectangle>

                                                </Border>

                                                <Button Width="40" Height="30" 
                                                        Grid.Column="1" Grid.Row="0"
                                                        Style="{StaticResource 
                                                               crumbControlButtonStyle}"
                                                        Tag="{Binding}" Margin="5"
                                                        ToolTip="Remove Crumb" 
                                                        Click="RemoveCrumb_Click" 
                                                        VerticalAlignment="Center" 
                                                        HorizontalAlignment="Left">
                                                    <Image Source="../Images/trash.png" 
                                                           Width="22" Height="22"
                                                           HorizontalAlignment="Center" 
                                                           VerticalAlignment="Center">
                                                        <Image.Effect>
                                                            <DropShadowEffect 
                                                               Color="Black" Direction="320" 
                                                               Opacity="0.8" BlurRadius="12" 
                                                               ShadowDepth="8"/>
                                                        </Image.Effect>
                                                    </Image>
                                                </Button>
                                                
                                                <Button Width="40" Height="30" 
                                                        Grid.Column="1" Grid.Row="1"
                                                        Style="{StaticResource 
                                                               crumbControlButtonStyle}"
                                                        Tag="{Binding}" Margin="5"
                                                        ToolTip="View Crumb" 
                                                        Click="ViewCrumb_Click" 
                                                        VerticalAlignment="Center" 
                                                        HorizontalAlignment="Left">
                                                    <Image Source="../Images/view.png" 
                                                           Width="22" Height="22"
                                                           HorizontalAlignment="Center" 
                                                           VerticalAlignment="Center">

                                                        <Image.Effect>
                                                            <DropShadowEffect Color="Black" 
                                                              Direction="320" 
                                                              Opacity="0.8" 
                                                              BlurRadius="12" 
                                                              ShadowDepth="8"/>
                                                        </Image.Effect>
                                                    </Image>
                                                </Button>

                                            </Grid>

                                            <DataTemplate.Triggers>
                                                

                                                <Trigger 
                                                 Property="ItemsControl.AlternationIndex" 
                                                 Value="0">
                                                    <Setter TargetName="imgBord" 
                                                       Property="Background" 
                                                       Value="LightGray"/>
                                                </Trigger>
                                                
                                                <Trigger 
                                                  Property="ItemsControl.AlternationIndex" 
                                                  Value="1">
                                                    <Setter TargetName="grid" 
                                                       Property="Background" 
                                                       Value="LightGray"/>
                                                </Trigger>

                                            </DataTemplate.Triggers>

                                        </DataTemplate>
                                    </ItemsControl.ItemTemplate>
                                </ItemsControl>
                            </local:FrictionScrollViewer>
                        </Grid>
                    </Border>
                </Popup>
            </Grid>
        </ToggleButton>
    </DataTemplate>
</ItemsControl.ItemTemplate>

And this is what that all looks like when running:

Note About BreadCrumb Styles

If you do not like my styles, all you have to do is hack the BreadCrumbViewManager.xaml, or look in the BreadCrumbControl/Resources/AppStyles.xaml, which is where all the BreadCrumbViewManager styles are kept.

Supporting IsDirty

Since there are multiple crumbs (Views) all being held in active memory by the BreadCrumbViewManager, it would be totally understandable if the user were to accidentally request a maintained crumb (View) to be closed, when there had already been a change of state made to the crumb (remember, my demo crumbs are simple, imagine a customer record 1/2 way through an edit and loosing that edit), where the current crumb may have been edited in some way. Loosing these edits could generally be considered bad news. So what can we do about it? Well, as part of the contract between the BreadCrumbViewManager and the crumbs, it will show the IBreadCrumbView which looks like this:

using System;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

namespace BreadCrumbControl
{
    public interface IBreadCrumbView
    {
        BitmapImage CrumbImageUrl { get; }
        String DisplayName { get; }
        Boolean IsDirty { get; }
    }
}

Note the IsDirty property that is expected to be implemented by your own Views. This flag is examined when the BreadCrumbViewManager attempts to remove a crumb. Basically, a check is done to see if the crumb being requested to remove is IsDirty, and if it is, the user must confirm the removal operation, as shown in this excerpt from the BreadCrumbViewManager code we saw earlier:

private void RemoveCrumb_Click(object sender, RoutedEventArgs e)
{
    try
    {
        WrappedIBreadCrumbView crumbToRemove = 
            (WrappedIBreadCrumbView)((Button)sender).Tag;

        IBreadCrumbView currentCrumbView =
            (IBreadCrumbView)transitionBox.Content;

        IBreadCrumbView crumbToRemoveView = 
            (IBreadCrumbView)crumbToRemove.BreadCrumbItem;

        if (crumbToRemoveView.IsDirty)
        {
            if (MessageBox.Show(
                "The current crumb is Dirty, Possible changes exist " +
                "\r\nDo you really want to remove it",
                "Confirm Remove", MessageBoxButton.YesNo, 
                MessageBoxImage.Question) == MessageBoxResult.Yes)
            {
                CheckForCurrentCrumbAndConfirmRemoval(crumbToRemove, 
                    currentCrumbView, crumbToRemoveView);
            }
        }
        else
        {
            CheckForCurrentCrumbAndConfirmRemoval(crumbToRemove, 
                currentCrumbView, crumbToRemoveView);
        }
    }
    catch
    {
        //not much we can do about it
    }
}

There is just one bit of magic there, which is that the original IBreadCrumbView wrapper (WrappedIBreadCrumbView) is obtained from the deleted Button's Tag property. This should have been explained in the previous section (I hope).

So all that you need to do is make sure that the IBreadCrumbView.IsDirty is implemented correctly in your Views that you wish to be crumbs for the BreadCrumbViewManager. Use common sense; when some state changes, set IsDirty=true.

Supported Transitions

Credit where credit is due, I am not responsible for the transitions you see inside of this project; they are all thanks to the rather splendid Transitionals.Dll, which was part of the work done as research by Microsoft.

The BreadCrumbViewManager supports the following transitions, which directly map (through my own enum) to Transitions within Transitionals.Dll:

public enum TransitionType
{
    FadeAndGrow=1,
    Translate,
    FadeAndBlur,
    Rotate
}

There are many more transitions apart from the ones I decided best suited my needs; you can learn more about this by downloading and playing with Transitionals from its CodePlex site: http://transitionals.codeplex.com/.

So how does Transitionals actually work? I tell you, it is really simple; all we need to do is the following:

Add a TransitionElement to Some XAML

<transitionalsControls:TransitionElement x:Name="transitionBox" />

Pick Your Transition

transitionBox.Transition = new FadeAndBlurTransition();

And that is all there is to using Transitionals really. Isn't it nice? As I say, there are loads more Transitions to choose from; I just felt they were not a good fit for what I was trying to do. I'll leave it up to you to explore the rest.

How to Use it in Your Own Project

To use the BreadCrumbControl in your own apps is as easy as following these three steps:

Step 1: Reference the BreadCrumbControl DLL

Make sure you have a reference to BreadCrumbControl.Dll.

Step 2: Ensure that any Views (crumbs) are Really BreadCrumb Ready

You just need to make sure that any View that you would like to use in the BreadCrumbViewManager can be recognized as being valid BreadCrumbs. This is achieved by implementing BreadCrumbControl.IBreadCrumbView. This BreadCrumbControl.IBreadCrumbView looks like this:

using System;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

namespace BreadCrumbControl
{
    public interface IBreadCrumbView
    {
        BitmapImage CrumbImageUrl { get; }
        String DisplayName { get; }
        Boolean IsDirty { get; }
    }
}

And here is an example View showing what is needed to implement this interface for a typical View to make it BreadCrumbViewManager ready.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using BreadCrumbControl;
using System.IO;

namespace WPFBreadCrumbSystem
{
    public partial class ImageControl : UserControl, IBreadCrumbView
    {
        private static BitmapImage crumbImageUrl;
        private Boolean isDirty;

	public ImageControl()
	{
	    this.InitializeComponent();
            this.DisplayName = "Dummy View : Simple Image Browser";
	}

    #region IBreadCrumbView Members

    public String DisplayName { get; private set; }

	//The CrumbImageUrl should point to an Image in your Assembly using a fully
	//qualified pack syntax Url, so that the BreadCrumbControl Dll can create an
	//Image for this type of BreadCrumb
    public BitmapImage CrumbImageUrl
    {
        get
        {
            if (crumbImageUrl == null)
            {
                crumbImageUrl = new BitmapImage(
                    new Uri("pack://application:,,,/" + 
                        "WPFBreadCrumbSystem;component/Images/pictures.png"));
            }
            return crumbImageUrl;
        }
    }

    public Boolean IsDirty
    {
        get
        {
            return isDirty;
        }
    }

    #endregion

	}
}

To see where these bits of the BreadCrumbControl.IBreadCrumbView are used, consider the following image:

In the dummy demo crumbs (Views), ImageControl / MusicControl, you will see that the BitmapImage is only created once per type, using a static field which is only initialised once (as it's static, it's per type based field). I would strongly recommend this approach, as the BreadCrumbViewManager really only needs a single Image for each type of crumb added. Have a look at the demo app's two Views, and the code snippet above, and you will see what I mean; try and follow this practice if you can.

Step 3: Initialising and Using the BreadCrumbControl Instance

There are many many ways you could go about creating an instance of the BreadCrumbViewManager and adding it to your WPF app's VisualTree. I shall show a code-behind way, but you could use a XAML approach if you prefer. As long as the BreadCrumbViewManager is added to your VisualTree and you can get a handle to that instance, it's all good, whatever works for you really.

Like I say, I am using a tiny bit of code-behind, and here is that code-behind. This code-behind shows how to setup the BreadCrumbViewManager and also how to add crumbs (Views) to it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using BreadCrumbControl;

namespace WPFBreadCrumbSystem
{
    public partial class Window1 : Window
    {
	    //The BreadCrumb control instance
        public BreadCrumbViewManager ViewManager { get; private set; }

        public Window1()
        {
            InitializeComponent();
            InitialiseBreadCrumbViewManager();
        }

	    //Initialise the BreadCrumb control
        private void InitialiseBreadCrumbViewManager()
        {
            ViewManager = new BreadCrumbViewManager();
            mainContent.Content = ViewManager;
        }

        //Add a MusicControl crumb to the BreadCrumb control
        private void btnMusic_Click(object sender, RoutedEventArgs e)
        {
            MusicControl ctrl = new MusicControl();
            ViewManager.AddCrumb(ctrl);
        }

		//Add a ImageControl crumb to the BreadCrumb control
        private void btnPictures_Click(object sender, RoutedEventArgs e)
        {
            ImageControl ctrl = new ImageControl();
            ViewManager.AddCrumb(ctrl);
        }
    }
}

Note: If you are using the MVVM pattern and are concerned with the usage of code-behind here to add new crumbs into the BreadCrumbViewManager control, there is a way you could do this without using code-behind, such as:

  • Wrap the BreadCrumbViewManager control, up in a BreadCrumb service, that is used to firstly create an application wide instance of a BreadCrumbViewManager control, which is then added to the UI. The BreadCrumb service could also allow the user to add new crumbs to the BreadCrumbViewManager using another method on the BreadCrumb service which could accept a IBreadCrumbView, and adds it the internal instance (inside of the BreadCrumb service) of the BreadCrumbViewManager. This would be easy to do; in fact, it is so easy I have actually created another demo project (on the train on the way home tonight) in the attached code that showcases the BreadCrumbViewManager being used in an MVVM based application.

What About MVVM

As I just stated, the key to using the BreadCrumbViewManager in an MVVM application lies in the development of a BreadCrumb service. Once you have that, it's all plain sailing. Here is the service interface (contract) for a Breadcrumb service that may be used in your own MVVM apps:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BreadCrumbControl;

namespace WPFBreadCrumbSystemMVVM
{
    /// <summary>
    /// This interface defines the options available
    /// when using the BreadCrumbManagerService within a
    /// MVVM app
    /// </summary>
    public interface IBreadCrumbManagerService
    {
        /// <summary>
        /// Registers a type through a key.
        /// </summary>
        /// <param name="key">Key for the UI Crumb</param>
        /// <param name="winType">Type which implements dialog</param>
        void Register(string key, Type winType);

        /// <summary>
        /// Obtains the instance of the <see cref="BreadCrumbViewManager">
        /// BreadCrumbViewManager</see>
        /// </summary>
        BreadCrumbViewManager CrumbManager { get; }

        /// <summary>
        /// Will attempt to show the requested view in the 
        /// BreadCrumbViewManager
        /// </summary>
        /// <param name="viewType">The String name of the view that was used
        /// to register the view</param>
        void ShowViewInBreadCrumbControl(String viewType);
    }
}

And here is what the actual service implementation looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BreadCrumbControl;
using System.Windows.Controls;

namespace WPFBreadCrumbSystemMVVM
{
    /// <summary>
    /// This class implements the 
    /// IBreadCrumbManagerService for WPF purposes.
    /// </summary>
    public class BreadCrumbManagerService 
        : IBreadCrumbManagerService
    {
        #region Data
        private readonly Dictionary<string, Type> registeredViews;
        private static BreadCrumbViewManager breadCrumbViewManager;
        #endregion

        #region Ctor
        /// <summary>
        /// Constructor
        /// </summary>
        public BreadCrumbManagerService()
        {
            registeredViews = new Dictionary<string, Type>();
        }

        static BreadCrumbManagerService()
        {
            breadCrumbViewManager = new BreadCrumbViewManager();
        }
        #endregion

        #region IBreadCrumbManagerService Members

        /// <summary>
        /// Registers a type through a key.
        /// </summary>
        /// <param name="key">Key for the UI dialog</param>
        /// <param name="viewType">Type which implements IBreadCrumbView</param>
        public void Register(string key, Type viewType)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");
            if (viewType == null)
                throw new ArgumentNullException("viewType");
            if (!typeof(IBreadCrumbView).IsAssignableFrom(viewType))
                throw new ArgumentException(
                    "viewType must be of type IBreadCrumbView");

            lock (registeredViews)
            {
                registeredViews.Add(key, viewType);
            }
        }

        /// <summary>
        /// Obtains the instance of the <see cref="BreadCrumbViewManager">
        /// BreadCrumbViewManager</see>
        /// </summary>
        public BreadCrumbControl.BreadCrumbViewManager CrumbManager
        {
            get { return breadCrumbViewManager; }
        }

        /// <summary>
        /// Will attempt to show the requested view in the 
        /// BreadCrumbViewManager
        /// </summary>
        /// <param name="viewType">The String name of the view that was used
        /// to register the view</param>
        public void ShowViewInBreadCrumbControl(String viewType)
        {
            if (registeredViews.ContainsKey(viewType))
            {
                IBreadCrumbView newCrumb = 
                    (IBreadCrumbView)Activator.CreateInstance(
                       registeredViews[viewType]);
                CrumbManager.AddCrumb(newCrumb);

            }
            else
            {
                throw new ArgumentException(
                    "viewType must be the same as the string used to " + 
                    "register, and it must be registered before calling " +
                    "ShowViewInBreadCrumbControl");
            }
        }
        #endregion
    }
}

For completeness, let me outline the steps associated with using this BreadCrumbManagerService in your own MVVM application.

Step1 : Choose Your ViewModel Framework

Obviously, this was an extremely easy choice for me to make; I simple chose my own MVVM Framework, Cinch.

Step 2: Add / Configure the BreadCrumbManagerService

Before you can use the BreadCrumbManagerService, you must add the types of the crumbs (Views) that you are expecting it to show. These are set as strings, such that the ViewModel can use simple string names and so that the ViewModel (which could be in a different DLL) knows nothing about the actual Views. Separation of concerns and all that.

For Cinch, this is best done in App.xaml.cs within the OnStarted() override, as shown below. Obviously, if you are not using Cinch (you really should be, Ha Ha), you will have to work out how to add the BreadCrumbManagerService to your own service locator method.

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
using Cinch;
using System.Windows.Threading;

namespace WPFBreadCrumbSystemMVVM
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            //Create and initial new services
            BreadCrumbManagerService breadCrumbService = 
                new BreadCrumbManagerService();

            //And add acceptable crumb type names, to allow 
            //ViewModel to create new instance
            //by simply passing in a string
            breadCrumbService.Register("ImageControl", 
                typeof(ImageControl));
            breadCrumbService.Register("MusicControl",
                typeof(MusicControl));

            //Add the service
            ViewModelBase.ServiceProvider.Add(typeof(IBreadCrumbManagerService), 
                breadCrumbService);

            Application.Current.Dispatcher.UnhandledException 
                += Dispatcher_UnhandledException;
        }

        private void Dispatcher_UnhandledException(object sender,
            DispatcherUnhandledExceptionEventArgs e)
        {
            Exception ex = e.Exception;
            MessageBox.Show("A fatal error occurred " + ex.Message);
            e.Handled = true;
            Environment.Exit(-1);
        }
    }
}

Step 3: Make Sure the BreadCrumbViewManager is Part of the VisualTree

So now that we have a new BreadCrumbManagerService, we need to make sure that the contained BreadCrumbViewManager is part of the VisualTree. This can easily be done using a single line of code-behind, as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using BreadCrumbControl;
using Cinch;

namespace WPFBreadCrumbSystemMVVM
{

    public partial class Window1 : Window
    {

        public Window1()
        {
            this.DataContext = new Window1ViewModel();
            InitializeComponent();
            InitialiseBreadCrumbViewManager();
        }


        private void InitialiseBreadCrumbViewManager()
        {
            mainContent.Content = 
                ViewModelBase.ServiceProvider
		    .Resolve<IBreadCrumbManagerService>()
                    .CrumbManager;
        }
    }
}

Step 4: Creating a New View Instance From a ViewModel

The last step of the puzzle is to allow the ViewModel to create a new instance of a particular type of View. Now, we have to remember that the main tenant of MVVM is separation of concerns. To ensure this, I normally have my ViewModels in a separate Dll (say, ViewModels.Dll) and have this ViewModels.Dll referenced by my UI DLL. As such, there can not be a reference from ViewModel.Dll to the UI DLL, as it would cause a circular reference. I like this restriction, as it aids my design. But it does also mean that trying to create a new crumb (View) to add to our BreadCrumbManagerService from our ViewModel obviously means that the ViewModel can not know about a View's type, as that would require a ViewModel to know something about a View, which as I just stated would lead to circular references. So we need to use a loosely coupled approach. Basically, all crumbs are registered using string names representing the crumb type, by some UI code (App.xaml.cs in my case), and then the ViewModel code can request a new instance of a crumb to be created, by using a string that was used when a type of crumb (View) was first registered. Confused? This small little ViewModel may help; this is all there is to it really.

using System;
using Cinch;

namespace WPFBreadCrumbSystemMVVM
{
    public class Window1ViewModel : Cinch.ViewModelBase
    {
        #region Data
        private SimpleCommand addMusicControlCommand;
        private SimpleCommand addImageControlCommand;
        private IBreadCrumbManagerService breadCrumbManagerService;
        #endregion

        #region Ctor
        public Window1ViewModel()
        {
            #region Get Services

            breadCrumbManagerService = 
                this.Resolve<IBreadCrumbManagerService>();

            #endregion

            #region Create Commands

            //Create Add Music Control Command
            addMusicControlCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => true,
                ExecuteDelegate = x => 
                    ExecuteAddMusicControlCommand()
            };

            //Create Add Image Control Command
            addImageControlCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => true,
                ExecuteDelegate = x => 
                    ExecuteAddImageControlCommand()
            };

            #endregion

        }
        #endregion

        #region Public Properties
        /// <summary>
        /// AddMusicControlCommand : Add Image command
        /// </summary>
        public SimpleCommand AddMusicControlCommand
        {
            get { return addMusicControlCommand; }
        }

        /// <summary>
        /// AddImageControlCommand : Add Image command
        /// </summary>
        public SimpleCommand AddImageControlCommand
        {
            get { return addImageControlCommand; }
        }
        #endregion

        #region Command Implementation

        #region ExecuteAddMusicControlCommand
        /// <summary>
        /// Executes the AddMusicControlCommand : Tells 
        /// BreadCrumbService to add a new MusicControl
        /// </summary>
        private void ExecuteAddMusicControlCommand()
        {
            breadCrumbManagerService.
                ShowViewInBreadCrumbControl("MusicControl");
        }
        #endregion

        #region ExecuteAddImageControlCommand
        /// <summary>
        /// Executes the AddImageControlCommand : Tells 
        /// BreadCrumbService to add a new ImageControl
        /// </summary>
        private void ExecuteAddImageControlCommand()
        {
            breadCrumbManagerService.
                ShowViewInBreadCrumbControl("ImageControl");
        }
        #endregion

 
        #endregion
    }
}

The only things to go back and have a look at are:

  1. When the crumbs were registered in app.xaml.cs, they were registered with the same strings that the ViewModel uses.
  2. That the BreadCrumbManagerService creates a new instance of a crumb (View) using Activator.CreatInstance( ).

See MVVM and the BreadCrumbControl work very nicely together, no fuss.

Known Issues

That when using a complex'ish breadcrumb crumb/views and the 3D rotate transition, it's a bit sluggish. However, the other three transitions are all fine. It's just something to bear in mind.

That's it Folks

Anyways folks, that is all I have to say for now. Although at its core this article is a very simple idea, I am really pleased with the results, and do think it's really easy to use in your own project. As such, I sure would really appreciate some votes, and some comments if you feel this control will help you out in your own WPF projects.

License

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

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionOutstanding Work PinmemberBishman4-May-12 4:26 
AnswerRe: Outstanding Work PinmvpSacha Barber4-May-12 4:39 
GeneralRe: Outstanding Work PinmemberBishman4-May-12 5:44 
GeneralRe: Outstanding Work PinmemberBishman4-May-12 8:31 
On more question for you Sacha, I am just wondering with BreadCrumbViewManager there are alot of code in the code behind. Is that done intentially? Meaning why did you not create a ViewModel for the class to wrap all the functionality?
 
Also, I was wondering how would you control the "Confirmation" message box from the viewmodel if I am using in MVVM. Basically wondering if I have my own "Dialog Service" and I want the BreadCrumbViewManager to use that service how can I do that?
 
Thanks again, and it is an outstanding work.
GeneralRe: Outstanding Work PinmemberBishman4-May-12 9:21 
Questionuseless ;} Pinmemberradioman.lt26-Oct-11 20:32 
AnswerRe: useless ;} PinmvpSacha Barber26-Oct-11 21:47 
GeneralRe: useless ;} Pinmemberradioman.lt27-Oct-11 0:06 
GeneralGreat PinmemberCIDev20-May-11 6:40 
GeneralRe: Great PinmvpSacha Barber20-May-11 6:46 
GeneralReally cool PinmentorBrij13-Apr-10 9:12 
GeneralRe: Really cool PinmvpSacha Barber13-Apr-10 10:38 
GeneralGreat, But ... PinmemberMuhammad Gouda13-Apr-10 5:00 
GeneralRe: Great, But ... PinmvpSacha Barber13-Apr-10 5:24 
GeneralAs usual PinmemberDr.Luiji12-Apr-10 0:57 
GeneralRe: As usual PinmvpSacha Barber12-Apr-10 1:16 
GeneralRe: As usual PinmemberDr.Luiji12-Apr-10 2:42 
GeneralRe: As usual PinmvpSacha Barber12-Apr-10 3:46 
GeneralRe: As usual PinmvpSacha Barber14-Apr-10 4:22 
GeneralGeate job PinmemberNavnath R. Kale6-Apr-10 2:24 
GeneralRe: Geate job PinmvpSacha Barber6-Apr-10 3:00 
GeneralAwesome.... PinmvpAbhijit Jana2-Apr-10 8:02 
GeneralRe: Awesome.... PinmvpSacha Barber2-Apr-10 20:49 
GeneralGreat. Pinmemberjayantbramhankar19-Mar-10 9:27 
GeneralRe: Great. PinmvpSacha Barber21-Mar-10 5:55 

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
Web04 | 2.8.140721.1 | Last Updated 16 Mar 2010
Article Copyright 2010 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid