WPF:漂亮的 View 面包屑管理器






4.97/5 (74投票s)
WPF 的一个可重用面包屑控件。
目录
引言
你们中的一些人可能听说过 Billy Hollis 以及他参与 dnrTV 的工作,还有一个他之前向公众展示的 WPF 演示应用程序。基本上,它展示了一个很酷的面包屑类型控件,它允许将 WPF 控件(称为视图)的独立实例存储在一个类似面包屑的控件中。然后用户可以选择新的视图,这些视图会被添加到面包屑控件中,面包屑控件会显示活动项的数量(基本上,此时面包屑控件中的任何内容都保存在内存中),并且还允许用户重新加载这些活动的视图。由 Billy Hollis 演示的面包屑控件还展示了当前面包屑控件视图与用户选择的新视图之间的过渡效果。
现在,我真的很喜欢这个,但是由于 Billy Hollis 只是在展示他所做工作的视频,因此没有源代码可用。这很可惜,因为我从未见过类似的东西,直到最近,我所在的 http://karlshifflett.wordpress.com 发布了他的 BBQ Shack/Ocean 2 框架。Karl 基本上创建了一个视图服务,他称之为“非线性应用程序导航 - 视图管理器服务”,您可以在 Karl 的博客文章中阅读有关它的信息。
说了这么多,轮到我这个小人物做什么了呢?好吧,我确实通读了 Karl 的博客文章,但我仍然觉得我可以尝试创建一个我自己的可重用面包屑控件,让用户能够快速地将这种能力添加到他们自己的 WPF 应用程序中。
我必须承认,尽管我阅读了 Karl 的博客文章,但我并没有检查代码,所以我认为我的代码几乎不会有任何重叠。我基本上是从头开始编写的,并尝试创建一个 BreadCrumb
控件,该控件能够实现我期望的可重用面包屑控件的所有功能,并让用户以最少的麻烦来使用它。
本文代表了我为 WPF 创建一个高度可重用的面包屑控件所做的工作。
必备组件
我很高兴地告诉您,运行和使用附带代码所需的全部就是 Visual Studio 2008 或更高版本,以及已安装的 .NET 3.5 SP1。
演示视频
本文最好通过视频来演示(注意:没有音频),但将在本文中详细解释视频中的所有工作。
只需点击此图像,它将带您进入一个显示视频的新页面。但在您这样做之前,我强烈建议您阅读视频中需要注意的要点,因为通过关注这些要点,您将更好地理解本文附带的代码是如何工作的。
以下是观看视频时应注意的一些事项
- BreadCrumb 控件存储可回忆的视图实例,这些视图由用户在导航各种视图(从现在开始称为 crumbs)时添加到 BreadCrumb 控件中。
- 对于添加到 BreadCrumb 控件的每个 crumb(视图),都会添加一个快速链接 crumb 条目,以便用户能够非常快速地导航回先前查看过的 crumb。
- 每个包含的 crumb 的实时迷你视觉表示都可以在每个添加的 crumb 类型弹出窗口中找到。对于 BreadCrumb 控件中的每个 crumb,用户将看到 crumb 的当前视觉表示。这使用户可以轻松地识别出 BreadCrumb 中包含的 crumb(视图)中他们可能希望返回并重新加载的 crumb。
- 用户可以选择查看或删除 BreadCrumb 中保存的 crumb(视图)。
- BreadCrumb 控件有一个检查“
IsDirty
”标志的概念,该标志可用于在从 BreadCrumb 控件中删除 crumb 之前提醒用户。 - 在 BreadCrumb 中,有多种过渡效果可供选择,用于从当前 crumb(视图)切换到新请求的 crumb(视图),允许用户选择他们偏好的过渡效果。
如果您第一次观看视频时没有注意到所有这些,我建议您再次观看视频,因为它将帮助您更好地理解本文的其余部分。
关于演示应用程序的一点说明
在我们深入研究代码之前,我想先讨论一下附带的演示代码及其结构的一个小方面。
当您在 Visual Studio 中加载附带的代码时,您会看到类似这样的内容
您会注意到有三个项目(C#,抱歉 VB 用户)。原因在于 _BreadCrumbControl_ 是一个可重用的 DLL,它是自包含的,并且拥有所有代码和 WPF 样式/模板/转换器等。因此,如果您不喜欢它的视觉风格(实际上是我的风格,因为我是它的所有者/创建者,我诞生了这个怪物),那么 BreadCrumbControl
DLL 是开始进行一些重新着色的地方(如果您愿意的话)。
第二个项目 _BreadCrumbSystem_ 只是一个一次性的演示应用程序,它托管一个 BreadCrumbControl
的实例,并包含两个虚拟 crumb,它们仅用于展示 _BreadCrumbControl_ DLL 的功能。
现在,当我说是“一次性”演示代码时,我是认真的,但其中有一些代码可以引导您开始在您自己的项目中使用 BreadCrumbControl
,但稍后我将讲到这一点。实际上它非常简单,只需要 3-4 行窗口代码,以及每个您想“面包屑化”的视图大约两个属性,但稍后详述。
为了让您清楚地了解哪些是演示代码,哪些是本文的主旨,让我们考虑以下图像
上面的图像包含我们需要考虑的三个区域
- 有一个宿主窗口(
WPFBreadCrumbSystem.Window1
),其中包含按钮,用于将两个虚拟视图添加到托管的BreadCrumbControl.BreadCrumbViewManager
实例中。窗口在此显示为紫色,但显然也包括绿色区域,这是BreadCrumbControl
的实际实例。该窗口基本上是可抛弃的;然而,它确实向您展示了让BreadCrumbControl.BreadCrumbViewManager
在您自己的应用程序中工作所需的步骤。不用担心,我稍后会全部解释。 BreadCrumbControl
的实际实例,如上图绿色所示,目前正在显示演示应用程序的一个 crumb/视图(基本上是为展示BreadCrumbControl.BreadCrumbViewManager
功能而创建的)。- 一个演示 crumb/视图,如上图蓝色区域所示。虽然这些 crumb/视图基本上是可抛弃的,但它们确实展示了让
BreadCrumbControl.BreadCrumbViewManager
在您自己的应用程序中工作所需的步骤。不用担心,我稍后会全部解释。附带的代码包含两个一次性的演示 crumb/视图:ImageControl
、MusicControl
。
第三个项目 _BreadCrumbSystemMVVM_ 用于演示 _BreadCrumbControl_ DLL 在 MVVM 应用程序中的功能。
它是如何工作的
本节及其子节将介绍本文中介绍的 BreadCrumbViewManager
控件的内部工作原理。
基本思路
其核心是一个名为 BreadCrumbViewManager
的控件,它控制 crumb(视图)的显示/隐藏,维护面包屑路径,并允许用户在四种可用过渡效果之间进行选择。用户可以自由使用它们来显示先前访问过的 crumb。它实际上并不是一个复杂的控件,使 BreadCrumbViewManager
工作的主要底层机制其实是一个特殊的 ObservableDictionary
。
以下步骤概述了 BreadCrumbViewManager
的工作原理
BreadCrumbViewManager
应添加到其他窗口/用户控件的 VisualTree 中(我稍后会告诉您)。- 用户可以创建自己的 crumb,它们是实现了
BreadCrumbControl.IBreadCrumbView
的 UserControls,该接口在这里进行讨论。 - 然后用户可以将 crumb 添加到
BreadCrumbViewManager
中,当添加新 crumb 时,会发生几件事 - 会进行检查以确保 crumb 不为 null。
- 还会检查是否已经存在用于添加的 crumb 的
Type
的键(在我们将要讨论的字典中)。如果对于要添加的 crumb 的类型存在键,则将新 crumb 包装在WrappedIBreadCrumbView
中,然后添加到与当前 crumbType
的键关联的ObservableCollection<WrappedIBreadCrumbView>
中。如果当前 crumb 的类型不存在键,则使用 crumb 的Type
和一个新的ObservableCollection<WrappedIBreadCrumbView>
在字典中添加一个新条目,该条目包含一个单独的项,该项是围绕当前 crumb 的WrappedIBreadCrumbView
。
信不信由你,这几乎就是我们将 crumb 添加到 BreadCrumbViewManager
中所需的全部;其余的都归功于 Binding
的魔力。
以下是 BreadCrumbViewManager
的全部代码;看,它真的很不错,不是吗?
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
}
}
}
}
显然,为了实现所有这些魔力,需要许多类和相当多的 XAML,但其中一个主要类(除了 BreadCrumbViewManager
)使其成为可能的是 ObservableDictionary
,它允许 WPF 的 DataBinding 绑定到键/值对。现在,这个类我不能居功,它实际上来自 Dr WPF 的思想深处。
它是一个非常好的类,它在这里
/* 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
}
}
那么 ObservableDictionary
在 BreadCrumbViewManager
XAML 中是如何使用的呢?好吧,几乎整个 BreadCrumbViewManager
XAML 看起来是这样的
<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>
为了更清楚地理解这一点,请看下面的图像
从这张图中可以看到,BreadCrumbViewManager
由许多独立的区域组成:您有 Expander
区域,用户可以在其中选择当前要使用的过渡效果。然后是过渡容器,它包含当前 crumb 和切换到新请求 crumb 的过渡。然后是底部区域,这是一个具有摩擦力的 ScrollViwer
(参见 FrictionScrollViewer
),我在多个项目中使用过它。
FrictionScrollViewer
包含一个 ItemsControl
,它绑定到 BreadCrumbViewManager
的整个 DataContext
。不用猜就知道那个 DataContext
对象可能是什么……没错,就是 ObservableDictionary
。在 ItemsControl
中,ItemTemplate.DataTemplate
是一个 ToggleButon
。稍后我们将更多地了解 ToggleButton
的 ControlTemplate
是如何创建的。
一个面包屑的构成
那么 crumb 到底是什么?
简单来说,crumb 可以是任何您希望用户查看或重新查看的 UserControl
。这很好,但是我们如何使我们典型的视图(通常是 UserControl
实例)做好面包屑的准备呢?
那就太简单了,我们只需要实现一个非常简单的接口。BreadCrumbControl.IBreadCrumbView
,定义如下
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; }
}
}
我将简要讨论这个接口的各个部分,因为在文章结尾,我将详细介绍您需要做些什么才能在您的应用程序中使用 BreadCrumbControl
。
那么,从顶部开始
CrumbImageUrl
:是一个BitmapImage
类的实例。它应该包含一个指向图像文件的指针,BreadCrumbControl
可以使用该图像来显示要添加的 crumb 类型的Image
。由于BreadCrumbViewManager
控件只显示一种 crumb 类型的单个图像,因此可以进行一些优化,以确保我们只创建一个BitmapImage
类的实例来满足BreadCrumbControl.IBreadCrumbView
接口。DisplayName
:是一个简单的文本字符串,用于表示 crumb 的类型,例如“Simple Image View”就完全没问题。IsDirty
:是您可以在视图(crumb)中设置的一个属性,当发生更改时可以设置它。然后BreadCrumbViewManager
会在内部检查此标志,以确定在关闭用户请求关闭的 crumb 之前是否应询问用户。毕竟,如果他们更改了某些内容,可能需要在关闭视图之前返回并保存状态。
需要注意的是,当 crumb 被添加到 BreadCrumbViewManager
控件时,它们会被包装在一个名为 WrappedIBreadCrumbView
的新对象中,该对象只是为 BreadCrumbViewManager
控件提供了几个可绑定的属性。我将在下一节中详细介绍。
实时预览
我认为这个控件最引人注目的功能之一是您确实可以获得对 crumb(视图)所做的更改的实时预览缩略图。不相信我,再次查看 演示视频,并留意 crumb 缩略图的更改。我说,这太酷了。
那么它是如何实现的呢?嗯,实际上它有两个部分。
第一部分:VisualBrush
第一部分令人惊讶地简单,当我们向 BreadCrumbViewManager
添加新的 IBreadCrumbView
时,我们只需创建一个 WrappedIBreadCrumbView
,其中 WrappedIBreadCrumbView
看起来是这样的
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
}
}
请注意其中的 VisualBrush
,它创建了新添加的 crumb(视图)的标准 WPF VisualBrush
。
以下是在 BreadCrumbViewManager
中为新添加的 crumb 创建 WrappedIBreadCrumbView
的方法
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));
}
}
}
}
第二部分:一个漂亮的按钮模板
如果一个 VisualBrush
没有地方显示,那它有什么用呢?所以我们需要一个地方来显示它,对吧?幸运的是,我已经考虑到了这一点,并为 ToggleButton
提供了一个 ControlTemplate
,它显示了某个类型视图的所有历史 crumb(基本上是字典的键,我之前一直在唠叨)。
这是 ToggleButton
的 ControlTemplate
,这是 ItemsControl.ItemTemplate
的一部分,因此对于添加到 BreadCrumbViewManager
的每个唯一的视图类型(crumb)都会发生这种情况。
<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>
这就是运行时的样子
关于面包屑样式的注意事项
如果您不喜欢我的样式,您所要做的就是修改 _BreadCrumbViewManager.xaml_,或者查看 _BreadCrumbControl/Resources/AppStyles.xaml_,这里存放了所有 BreadCrumbViewManager
的样式。
支持 IsDirty
由于 BreadCrumbViewManager
正在积极地将多个 crumb(视图)保存在内存中,因此如果用户不小心请求关闭一个已维护的 crumb(视图),而该 crumb 已经发生了状态更改(请记住,我的演示 crumb 很简单,想象一下客户记录正在编辑中,然后丢失了该编辑),那么当前 crumb 可能以某种方式被编辑了。丢失这些编辑通常被认为是坏消息。那么我们该怎么办呢?嗯,作为 BreadCrumbViewManager
和 crumb 之间的约定的一部分,它将显示 IBreadCrumbView
,如下所示
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; }
}
}
注意 IsDirty
属性,它需要由您自己的视图来实现。当 BreadCrumbViewManager
尝试删除 crumb 时,会检查此标志。基本上,会检查要删除的 crumb 是否为 IsDirty
,如果是,则用户必须确认删除操作,如我们之前看到的 BreadCrumbViewManager
代码片段所示
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
}
}
这里只有一个魔法点,那就是原始的 IBreadCrumbView
包装器(WrappedIBreadCrumbView
)是从被删除的 Button
的 Tag
属性中获取的。这应该在上一节中解释了(我希望)。
所以,您需要做的就是确保 IBreadCrumbView.IsDirty
在您希望作为 BreadCrumbViewManager
的 crumb 的视图中正确实现。运用常识;当某些状态发生变化时,将 IsDirty=true
。
支持的过渡效果
归功于应有的功劳,我不对本项目中的过渡效果负责;它们都归功于相当出色的 _Transitionals.Dll_,这是 Microsoft 作为研究工作的一部分。
BreadCrumbViewManager
支持以下过渡效果,它们(通过我自己的枚举)直接映射到 _Transitionals.Dll_ 中的过渡效果
public enum TransitionType
{
FadeAndGrow=1,
Translate,
FadeAndBlur,
Rotate
}
除了我决定的最适合我需求的过渡效果之外,还有许多其他过渡效果;您可以通过下载并玩转 CodePlex 网站上的 Transitionals 来了解更多信息:http://transitionals.codeplex.com/。
那么 Transitionals 到底是如何工作的呢?我告诉您,它真的很简单;我们只需要做以下事情
将 TransitionElement 添加到 XAML
<transitionalsControls:TransitionElement x:Name="transitionBox" />
选择您的过渡效果
transitionBox.Transition = new FadeAndBlurTransition();
这就是使用 Transitionals 的全部内容,不是吗?很棒吧?正如我所说,还有很多其他的过渡效果可供选择;我只是觉得它们不太适合我想做的事情。我把探索其余部分的任务留给您。
如何在您自己的项目中运用它
要在您自己的应用程序中使用 BreadCrumbControl
,就像遵循这三个步骤一样简单
步骤 1:引用 BreadCrumbControl DLL
请确保您已引用 _BreadCrumbControl.Dll_。
步骤 2:确保任何视图(crumb)都已准备好进行面包屑化
您只需要确保您希望在 BreadCrumbViewManager
中使用的任何视图都可以被识别为有效的 BreadCrumbs。这是通过实现 BreadCrumbControl.IBreadCrumbView
来实现的。这个 BreadCrumbControl.IBreadCrumbView
看起来是这样的
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; }
}
}
这是一个示例视图,展示了实现此接口以使典型视图为 BreadCrumbManager
准备好的内容。
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
}
}
为了查看 BreadCrumbControl.IBreadCrumbView
的这些部分在哪里使用,请看下面的图像
在虚拟演示 crumb(视图)ImageControl
/ MusicControl
中,您会看到 BitmapImage
是每个类型只创建一次,使用一个静态字段,该字段只初始化一次(因为它是静态的,所以是每个类型的字段)。我强烈推荐这种方法,因为 BreadCrumbViewManager
实际上只需要为添加的每种 crumb 类型一个 Image
。请看看演示应用程序的两个视图,以及上面的代码片段,您就会明白我的意思;如果可能,请尝试遵循此做法。
步骤 3:初始化和使用 BreadCrumbControl 实例
您可以有很多方法来创建 BreadCrumbViewManager
的实例并将其添加到 WPF 应用程序的 VisualTree 中。我将展示一种代码隐藏的方式,但您也可以选择 XAML 方法。只要 BreadCrumbViewManager
被添加到您的 VisualTree 中,并且您可以获得该实例的句柄,一切都很好,无论哪种方式适合您。
如我所说,我使用了一点代码隐藏,这是我的代码隐藏。此代码隐藏展示了如何设置 BreadCrumbViewManager
以及如何向其中添加 crumb(视图)。
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);
}
}
}
注意:如果您使用的是 MVVM 模式,并且对这里使用代码隐藏将新 crumb 添加到 BreadCrumbViewManager
控件感到担忧,那么有一种方法可以在不使用代码隐藏的情况下做到这一点,例如
- 将
BreadCrumbViewManager
控件包装在一个 BreadCrumb 服务中,该服务首先创建一个应用程序范围的BreadCrumbViewManager
控件实例,然后将其添加到 UI 中。BreadCrumb 服务还可以允许用户通过 BreadCrumb 服务上的另一个方法添加新的 crumb,该方法可以接受一个IBreadCrumbView
,并将其添加到 BreadCrumb 服务内部的BreadCrumbViewManager
的实例中。这很容易做到;事实上,它如此简单,以至于我actually 已经创建了另一个演示项目(今晚回家路上在火车上),在附带的代码中展示了BreadCrumbViewManager
在 MVVM 应用程序中的使用。
关于 MVVM
正如我刚才所说,在 MVVM 应用程序中使用 BreadCrumbViewManager
的关键在于开发一个 BreadCrumb 服务。一旦有了它,一切都很顺利。以下是用于您自己的 MVVM 应用程序的面包屑服务的接口(契约)
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);
}
}
以下是实际的服务实现样子
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
}
}
为了完整起见,我将概述在您自己的 MVVM 应用程序中使用此 BreadCrumbManagerService
所需的步骤。
步骤 1:选择您的 ViewModel 框架
显然,对我来说这是一个非常简单的选择;我选择了自己使用的 MVVM 框架,Cinch。
步骤 2:添加/配置 BreadCrumbManagerService
在使用 BreadCrumbManagerService
之前,您必须添加您期望它显示的 crumb(视图)的类型。它们被设置为字符串,这样 ViewModel 就可以使用简单的字符串名称,并且 ViewModel(可能在不同的 DLL 中)对实际视图一无所知。关注点分离等等。
对于 Cinch,最好在 _App.xaml.cs_ 的 OnStarted()
重写中完成,如下所示。显然,如果您不使用 Cinch(您真的应该用,哈哈),您将不得不弄清楚如何将 BreadCrumbManagerService
添加到您自己的服务定位器方法中。
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);
}
}
}
步骤 3:确保 BreadCrumbViewManager 是 VisualTree 的一部分
现在我们有了一个新的 BreadCrumbManagerService
,我们需要确保其中包含的 BreadCrumbViewManager
是 VisualTree 的一部分。这可以通过一行简单的代码隐藏轻松完成,如下所示
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;
}
}
}
步骤 4:从 ViewModel 创建新的视图实例
解开谜题的最后一步是允许 ViewModel 创建一个特定类型视图的新实例。现在,我们必须记住 MVVM 的主要原则是关注点分离。为了确保这一点,我通常会将我的 ViewModels 放在一个单独的 DLL 中(例如,_ViewModels.Dll_),并将这个 _ViewModels.Dll_ 引用到我的 UI DLL 中。因此,_ViewModel.Dll_ 不能引用 UI DLL,因为它会导致循环引用。我喜欢这种限制,因为它有助于我的设计。但这同时也意味着,尝试从 ViewModel 创建一个新的 crumb(视图)来添加到我们的 BreadCrumbManagerService
中,显然意味着 ViewModel 不能知道视图的类型,因为这会要求 ViewModel 了解视图,正如我刚才所说,这会导致循环引用。所以我们需要使用一种松耦合的方法。基本上,所有的 crumb 都是通过字符串名称(代表 crumb 类型)由一些 UI 代码(在我的例子中是 _App.xaml.cs_)注册的,然后 ViewModel 代码可以通过使用注册 crumb 类型(视图)时使用的字符串来请求创建一个新的 crumb 实例。困惑吗?这个小 ViewModel 可能会有帮助;这基本上就是全部了。
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
}
}
唯一需要回头看看的是
- 当 crumb 在 _app.xaml.cs_ 中注册时,它们是以与 ViewModel 使用的字符串相同的字符串注册的。
BreadCrumbManagerService
使用Activator.CreatInstance( )
创建一个新的 crumb(视图)实例。
看到了吗,MVVM 和 BreadCrumbControl
非常好地协同工作,没有麻烦。
已知问题
当使用复杂的面包屑 crumb/视图和 3D 旋转过渡效果时,它会有些迟缓。然而,其他三种过渡效果都很好。这只是需要注意的一点。
就是这样,各位
好了各位,我现在就说这么多。尽管这个想法在核心上非常简单,但我对结果感到非常满意,并且认为它在您的项目中非常容易使用。因此,如果您觉得这个控件能帮助您完成 WPF 项目,我非常希望得到一些投票和评论。