WPF 中的活动按钮
根据按钮属性的值更改导航菜单中使用的按钮的外观。
引言
在本文中,我介绍了一种根据按钮属性的值更改导航菜单中使用的按钮外观的方法。
附带的源代码提供了一个 Visual Studio 解决方案,其中包含 2 个项目
- ActiveButtonDemoStart:作为基础的应用(请参阅“设置场景”一节)
- ActiveButtonDemo:最终的应用,即按钮外观会发生变化的那个
背景
我们将使用以下概念
- MVVM (http://msdn.microsoft.com/en-us/magazine/dd419663.aspx)
- 数据绑定 (http://msdn.microsoft.com/en-us/library/ms752347%28v=vs.110%29.aspx)
- 使用命令和各种视图模型进行导航
(http://rachel53461.wordpress.com/2011/12/18/navigation-with-mvvm-2/ https://codeproject.org.cn/Articles/72724/Beginning-a-WPF-MVVM-application-Navigating-betwee) - 附加(依赖)属性 (http://msdn.microsoft.com/en-us/library/ms749011%28v=vs.110%29.aspx)
- 标记扩展 (http://msdn.microsoft.com/en-us/library/ms752059%28v=vs.110%29.aspx#markup_extensions)
- 样式 (http://msdn.microsoft.com/en-us/library/ms745683(v=vs.110).aspx)
如果您不熟悉这些概念,请参考我链接到的文档。
使用代码
设置场景
我们的起点是一个简单的应用程序(在附带的解决方案中以“ActiveButtonDemoStart”项目的形式提供)
用户可以单击左侧窗格中的按钮,右侧窗格的内容会相应地更改。这使用 MVVM 进行了很好的实现。
假设您希望用户能够直观地了解他们在应用程序中的位置。您希望与右侧窗格中的内容对应的按钮具有不同的样式。您想要“活动按钮”的概念。
不幸的是,WPF 默认不提供此功能。幸运的是,它确实为我们提供了将自定义数据附加到 DependencyObject(如 Button)的功能,而无需对其进行子类化:附加属性。
附加属性
让我们看看实现
public class ButtonExtensions
{
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached(
"IsActive"
, typeof(bool)
, typeof(ButtonExtensions)
, new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetIsActive(DependencyObject element, bool value)
{
element.SetValue(ButtonExtensions.IsActiveProperty, value);
}
public static bool GetIsActive(DependencyObject element)
{
return (bool)element.GetValue(ButtonExtensions.IsActiveProperty);
}
}
我创建了一个名为 ButtonExtensions 的类。名称无关紧要。我将属性注册为附加属性。我还提供了一个 setter 和一个 getter 方法。XAML 解析器将使用它们来设置相对于 DependencyObject(在本例中为 Button)的值。
请注意,这与 Button 没有关系。我们也可以在其他元素上使用此属性。
我们在 XAML 中如下使用该属性
<Button Content="My CDs" Command="{Binding ChangePageCommand}" CommandParameter="{Binding MyCDsVM}" local:ButtonExtensions.IsActive="True" />
XML 前缀“local”在用户控件的起始标签中定义
xmlns:local="clr-namespace:ActiveButtonDemo"
我们现在可以在 XAML 中将按钮设置为活动状态,但这并不是我们想要的硬编码在标记中。我们需要一种方法来在运行时决定值是 true 还是 false。让我们使用一个标记扩展。
标记扩展
在标记扩展的实现中,我们可以使用自己的逻辑来生成属性的值(在本例中:IsActive 的 true 或 false)。我们将基于按钮的 Name 和 CurrentPageViewModel(请参阅 MainViewModel)的类型的名称来决定按钮是否处于活动状态。
我们首先需要引入一个约定。按钮 Name 的重要部分应等于视图模型类型名称的重要部分。重要部分是在删除前缀和后缀(如 btn、ViewModel 等)后剩余的字符串部分。
例如
Button.Name = "btnMyCDs"
视图模型类型名称 = “MyCDsViewModel”
=> 重要部分 = "MyCDs"
让我们来实现这个标记扩展。
public class ActiveButtonExtension : MarkupExtension
{
private DataContextFinder _dataContextFinder;
private string[] _preAndSuffixes;
private bool _subscribed;
public ActiveButtonExtension()
{
_preAndSuffixes = new string[] { "btn", "ViewModel" };
}
/// <summary>
/// Gets or sets the target Dependency Object from the service provider
/// </summary>
protected Button Button { get; set; }
/// <summary>
/// Gets or sets the target Dependency Property from the service provider;
/// </summary>
protected DependencyProperty IsValidProperty { get; set; }
/// <summary>
/// This is the only method that is needed for a MarkupExtension.
/// All the others are helper methods.
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public override object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if(pvt != null)
{
IsValidProperty = pvt.TargetProperty as DependencyProperty;
Button = pvt.TargetObject as Button;
_dataContextFinder = new DataContextFinder(Button, OnDataContextFound);
_dataContextFinder.FindDataContext();
if (_dataContextFinder.DataContext == null)
{
_dataContextFinder.SubscribeToChangedEvent();
}
else
{
OnDataContextFound();
}
}
return false;
}
private string GetSignificantPart(string name)
{
string result = name;
int position;
foreach (string item in _preAndSuffixes)
{
position = name.IndexOf(item);
if (position > -1)
{
if (position + item.Length == name.Length)
{
//item is a suffix
result = name.Substring(0, name.Length - item.Length);
}
else
{
//item is a prefix
result = name.Substring(position + item.Length);
}
break;
}
}
return result;
}
private void OnDataContextFound(){
if (string.IsNullOrWhiteSpace(Button.Name))
{
return;
}
string name = GetSignificantPart(Button.Name);
string typeName = null;
if (_dataContextFinder.DataContext != null)
{
var mainVM = _dataContextFinder.DataContext as MainViewModel;
if (mainVM != null)
{
string[] nameParts = mainVM.CurrentPageViewModel.GetType().FullName.Split(new string[] { "." }, StringSplitOptions.None);
typeName = GetSignificantPart(nameParts[nameParts.Length - 1]);
//event handler for currentview changed event (INotifyPropertyChanged)
if (!_subscribed)
{
mainVM.PropertyChanged += mainVM_PropertyChanged;
_subscribed = true;
}
}
}
if (typeName != null)
{
bool isActive = typeName.Equals(name);
UpdateProperty(isActive);
}
}
private void mainVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("CurrentPageViewModel"))
{
OnDataContextFound();
}
}
private void UpdateProperty(bool isActive)
{
if (Button != null && IsValidProperty != null)
{
Action update = () => Button
.SetValue(IsValidProperty, isActive);
if (Button.CheckAccess())
{
update();
}
else
{
Button.Dispatcher.Invoke(update);
}
}
}
}
您可以看到有很多内容。您可能还注意到 DataContextFinder 的存在。它存在的原因是,我们无法保证在调用 ProvideValue 方法时 DataContext 仍然可用。
我搜索了一下,找到了这些提供了解决方案的文章,但正如您所注意到的,这是一个相当迂回的路线:http://peteohanlon.wordpress.com/2012/11/21/of-mice-and-men-and-computed-observables-oh-my/
http://www.thomaslevesque.com/2009/07/28/wpf-a-markup-extension-that-can-update-its-target/
感谢各位作者!
我对其进行了自己的修改,将查找数据上下文的逻辑放在一个单独的类(DataContextFinder)中,因为它在其他情况下也可能很有用(不仅仅是标记扩展)。
现在,我们可以使用我们全新的标记扩展来修改 XAML,如下所示
<Button Content="My CDs" Name="btnMyCDs" Command="{Binding ChangePageCommand}"
CommandParameter="{Binding MyCDsVM}"
local:ButtonExtensions.IsActive="{local:ActiveButton}"/>
样式
是时候添加一些视觉效果了。毕竟,这才是我们一开始想要的。
<Style TargetType="Button" x:Key="ActiveButtonStyle">
<Setter Property="Background" Value="Yellow"></Setter>
</Style>
这是一个非常简单的样式,只是为了显示与普通按钮的一些区别。要使其有用,您需要为许多其他属性提供 setter。
类型转换器
由于 IsActive 属性的值(样式决策基于此)仅在运行时可知,因此我们必须找到一种动态应用此样式的方法。
起初,我考虑使用样式选择器(继承自 System.Windows.Controls.StyleSelector),但显然我们无法让 Button 使用一个。Button 没有设置 StyleSelector 的属性。
但是,我们可以通过数据绑定来分配样式。样式取决于 IsActive 属性,因此在我们的绑定表达式中,我们应该引用它。由于 IsActive(布尔值)和 Style(System.Windows.Style)属性的类型不匹配,我们需要一个类型转换器(我们也可以在绑定表达式中指定它)。这是一个实现 IValueConverter 接口的类。
class ActiveButtonStyleConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Uri resourceLocater = new Uri("/ActiveButtonDemo;component/Styles.xaml", System.UriKind.Relative);
ResourceDictionary resourceDictionary = (ResourceDictionary)Application.LoadComponent(resourceLocater);
bool isActive = bool.Parse(value.ToString());
return isActive ? resourceDictionary["ActiveButtonStyle"] as Style : resourceDictionary["ButtonStyle"] as Style;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
我们创建的样式应该放在资源字典中,因为我们必须能够在类型转换器中找到并使用这个样式。我称之为 Styles.xaml,它看起来像这样
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Button" x:Key="ActiveButtonStyle">
<Setter Property="Background" Value="Yellow"></Setter>
</Style>
<Style TargetType="Button" x:Key="ButtonStyle">
<Setter Property="Background" Value="Gray"></Setter>
</Style>
</ResourceDictionary>
每个 Button 的 XAML 如下
<Button Content="My CDs" Name="btnMyCDs" Command="{Binding ChangePageCommand}"
CommandParameter="{Binding MyCDsVM}"
local:ButtonExtensions.IsActive="{local:ActiveButton}"
Style="{Binding Path=(local:ButtonExtensions.IsActive), RelativeSource={RelativeSource Self}, Converter={StaticResource activeButtonStyleConverter}}"
/>
Style 属性通过数据绑定赋值。请注意,我们在绑定中使用 RelativeSource,因为绑定需要访问 Button(而不是数据上下文,在本例中为 MainViewModel)。我们通过指定 Path 来引用 IsActive 属性。通过使用数据绑定,当 IsActive 更改时,样式将自动更新。
正如您在 MainViewModel 的构造函数中看到的,我已经为 CurrentView 设置了一个值。这意味着在启动时总有一个视图处于活动状态。相应的按钮按照指定样式显示(带黄色背景)。不幸的是,标记扩展只评估一次。如果您单击另一个按钮,样式不会相应调整,因为 IsActive 属性的值没有改变。您可以通过将 Button 的 Content 属性绑定到 IsActive 属性来在运行时验证这一点。
Content="{Binding RelativeSource={RelativeSource Self}, Path=(local:ButtonExtensions.IsActive)}"
这是我们必须克服的最后一个问题。幸运的是,MainViewModel 类实现了 INotifyPropertyChanged。在 ActiveButtonExtension 中,找到 DataContext 后,我们可以订阅此事件。
if (!_subscribed)
{
mainVM.PropertyChanged += mainVM_PropertyChanged;
_subscribed = true;
}
处理程序非常简单
private void mainVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("CurrentPageViewModel"))
{
OnDataContextFound();
}
}
当 CurrentPageViewModel 的值更改时,将 IsActive 属性值设置为 true/false 的逻辑会被执行。当 IsActive 更改时,样式会通过数据绑定更新。
所以,我们做到了:当我们单击一个按钮时,它的外观就会改变!
结论
最初看似简单的需求涉及大量的代码。WPF 提供所有这些可能性是很好的,但说实话,它变得多么复杂让我感到困惑。
历史
- 2014-07-13:提交
- 2014-07-14:修复了一些错别字并添加了最终结果的截图