Table of Contents
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.
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.
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.
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:
- 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.
- 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).
- 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.
This section and its subsections will go through the inner workings of the BreadCrumbViewManager
control presented in this article.
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:
- The
BreadCrumbViewManager
should be added to the VisualTree of some other Window/UserControl (I will tell you about this later on).
- The user can create their own crumbs, which are UserControls that have implemented the
BreadCrumbControl.IBreadCrumbView
, which is discussed right here.
- 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
}
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
{
}
}
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
{
}
}
}
}
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:
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()
{
bool result = (Count > 0);
if (result)
{
_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)
{
return _keyedEntryCollection.Remove(key);
}
protected virtual bool SetEntry(TKey key, TValue value)
{
bool keyExists = _keyedEntryCollection.Contains(key);
if (keyExists &&
value.Equals((TValue)_keyedEntryCollection[key].Value))
return false;
if (keyExists)
_keyedEntryCollection.Remove(key);
_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 (index > -1)
FireEntryRemovedNotifications(entry, index);
index = GetIndexAndEntryForKey(key, out entry);
FireEntryAddedNotifications(entry, index);
}
}
private void FireEntryAddedNotifications(DictionaryEntry entry, int index)
{
FirePropertyChangedNotifications();
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)
{
FirePropertyChangedNotifications();
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()
{
FirePropertyChangedNotifications();
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">
-->
-->
-->
-->
-->
-->
</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 ToggleButton
s ControlTemplate
is created later on.
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.
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
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
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.
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
{
}
}
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
.
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.
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; }
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
{
public BreadCrumbViewManager ViewManager { get; private set; }
public Window1()
{
InitializeComponent();
InitialiseBreadCrumbViewManager();
}
private void InitialiseBreadCrumbViewManager()
{
ViewManager = new BreadCrumbViewManager();
mainContent.Content = ViewManager;
}
private void btnMusic_Click(object sender, RoutedEventArgs e)
{
MusicControl ctrl = new MusicControl();
ViewManager.AddCrumb(ctrl);
}
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.
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
{
public interface IBreadCrumbManagerService
{
void Register(string key, Type winType);
BreadCrumbViewManager CrumbManager { get; }
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
{
public class BreadCrumbManagerService
: IBreadCrumbManagerService
{
#region Data
private readonly Dictionary<string, Type> registeredViews;
private static BreadCrumbViewManager breadCrumbViewManager;
#endregion
#region Ctor
public BreadCrumbManagerService()
{
registeredViews = new Dictionary<string, Type>();
}
static BreadCrumbManagerService()
{
breadCrumbViewManager = new BreadCrumbViewManager();
}
#endregion
#region IBreadCrumbManagerService Members
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);
}
}
public BreadCrumbControl.BreadCrumbViewManager CrumbManager
{
get { return breadCrumbViewManager; }
}
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
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
BreadCrumbManagerService breadCrumbService =
new BreadCrumbManagerService();
breadCrumbService.Register("ImageControl",
typeof(ImageControl));
breadCrumbService.Register("MusicControl",
typeof(MusicControl));
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
addMusicControlCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x =>
ExecuteAddMusicControlCommand()
};
addImageControlCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x =>
ExecuteAddImageControlCommand()
};
#endregion
}
#endregion
#region Public Properties
public SimpleCommand AddMusicControlCommand
{
get { return addMusicControlCommand; }
}
public SimpleCommand AddImageControlCommand
{
get { return addImageControlCommand; }
}
#endregion
#region Command Implementation
#region ExecuteAddMusicControlCommand
private void ExecuteAddMusicControlCommand()
{
breadCrumbManagerService.
ShowViewInBreadCrumbControl("MusicControl");
}
#endregion
#region ExecuteAddImageControlCommand
private void ExecuteAddImageControlCommand()
{
breadCrumbManagerService.
ShowViewInBreadCrumbControl("ImageControl");
}
#endregion
#endregion
}
}
The only things to go back and have a look at are:
- When the crumbs were registered in app.xaml.cs, they were registered with the same strings that the ViewModel uses.
- 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.
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.
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.