Хотя это более старый вопрос, другие могут по-прежнему интересоваться ответом, как и я. Итак, вот мое решение. Он основан на некотором обратном проектировании справочного источника Microsoft, который я нашел ( Button и ButtonBase ). Я все еще новичок в WPF, поэтому много кода может быть ненужным, но это работает!
Этот код добавляет следующие функции в UserControl (кажется, что они тесно связаны друг с другом):
- IsDefault
- IsCancel
- Команда
- Событие клика
(все комментарии в коде сделаны MS)
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Automation.Peers;
using System.Security;
using System.Diagnostics;
[DefaultEvent("Click")]
public partial class MyButton : UserControl, ICommandSource {
#region "Private Variables"
// Cache valid bits
private ControlBoolFlags _ControlBoolField;
#endregion
#region "Constructors"
static MyButton()
{
EventManager.RegisterClassHandler(
typeof(MyButton),
AccessKeyManager.AccessKeyPressedEvent,
new AccessKeyPressedEventHandler(
OnAccessKeyPressed));
KeyboardNavigation.AcceptsReturnProperty.OverrideMetadata(
typeof(MyButton),
new FrameworkPropertyMetadata(
true));
// Disable IME on button.
// - key typing should not be eaten by IME.
// - when the button has a focus, IME's disabled status should
// be indicated as
// grayed buttons on the language bar.
InputMethod.IsInputMethodEnabledProperty.OverrideMetadata(
typeof(MyButton),
new FrameworkPropertyMetadata(
false,
FrameworkPropertyMetadataOptions.Inherits));
}
#endregion
#region "AccessKey"
private static void OnAccessKeyPressed(object sender,
AccessKeyPressedEventArgs e)
{
if (!e.Handled && e.Scope == null && e.Target == null) {
e.Target = sender as MyButton;
}
}
/// <summary>
/// The Access key for this control was invoked.
/// </summary>
protected override void OnAccessKey(AccessKeyEventArgs e)
{
if (e.IsMultiple) {
base.OnAccessKey(e);
} else {
// Don't call the base b/c we don't want to take focus
OnClick();
}
}
#endregion
#region "Click"
/// <summary>
/// Event correspond to left mouse button click
/// </summary>
public static readonly RoutedEvent ClickEvent =
EventManager.RegisterRoutedEvent(
"Click",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyButton));
/// <summary>
/// Add / Remove ClickEvent handler
/// </summary>
[Category("Behavior")]
public event RoutedEventHandler Click
{
add {
AddHandler(ClickEvent, value);
}
remove {
RemoveHandler(ClickEvent, value);
}
}
/// <summary>
/// This virtual method is called when button is clicked and
/// it raises the Click event
/// </summary>
private void BaseOnClick()
{
RoutedEventArgs locRoutedEventArgs = new RoutedEventArgs(
MyButton.ClickEvent,
this);
this.RaiseEvent(locRoutedEventArgs);
ExecuteCommandSource(this);
}
/// <summary>
/// This method is called when button is clicked.
/// </summary>
private void OnClick()
{
if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked)) {
AutomationPeer locPeer =
UIElementAutomationPeer.CreatePeerForElement(this);
if (locPeer != null) {
locPeer.RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked);
}
}
// base.OnClick should be called first. Our default command
// for Cancel Button to close dialog should happen after
// Button's click event handler has been called.
// If there Is excption And it Then 's a Cancel button and
// RoutedCommand is null,
// we will raise Window.DialogCancelCommand.
try {
BaseOnClick();
} finally {
// When the Button RoutedCommand is null, if it's a
// Cancel Button,
// Window.DialogCancelCommand will be the default command.
// Do not assign Window.DialogCancelCommand to
// Button.Command.
// If in Button click handler user nulls the Command,
// we still want to provide the default behavior.
if (Command == null && IsCancel) {
// Can't invoke Window.DialogCancelCommand directly.
// Have to raise event.
// Filed bug 936090: Commanding perf issue: can't
// directly invoke a command.
ExecuteCommand(DialogCancelCommand, null, this);
}
}
}
#endregion
#region "ClickMode"
/// <summary>
/// The DependencyProperty for the ClickMode property.
/// Flags: None
/// Default Value: ClickMode.Release
/// </summary>
public static readonly DependencyProperty ClickModeProperty =
DependencyProperty.Register(
"ClickMode",
typeof(ClickMode),
typeof(MyButton),
new FrameworkPropertyMetadata(
ClickMode.Release),
new ValidateValueCallback(
IsValidClickMode));
/// <summary>
/// ClickMode specify when the Click event should fire
/// </summary>
[Bindable(true), Category("Behavior")]
public ClickMode ClickMode
{
get {
return (ClickMode)GetValue(ClickModeProperty);
}
set {
SetValue(ClickModeProperty, value);
}
}
private static bool IsValidClickMode(object valClickMode)
{
ClickMode locClickMode = (ClickMode)valClickMode;
return locClickMode == ClickMode.Press
|| locClickMode == ClickMode.Release
|| locClickMode == ClickMode.Hover;
}
#endregion
#region "KeyDown"
/// <summary>
/// This is the method that responds to the KeyDown event.
/// </summary>
/// <param name="e">Event arguments</param>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (ClickMode == ClickMode.Hover) {
// Ignore when in hover-click mode.
return;
}
if (e.Key == Key.Space) {
// Alt+Space should bring up system menu, we shouldn't
// handle it.
if ((Keyboard.Modifiers &
(ModifierKeys.Control | ModifierKeys.Alt)) !=
ModifierKeys.Alt) {
if ((!IsMouseCaptured) &&
(object.ReferenceEquals(e.OriginalSource, this))) {
IsSpaceKeyDown = true;
CaptureMouse();
if (ClickMode == ClickMode.Press) {
OnClick();
}
e.Handled = true;
}
}
} else if (e.Key == Key.Enter
&& Convert.ToBoolean(GetValue(KeyboardNavigation.AcceptsReturnProperty))) {
if (object.ReferenceEquals(e.OriginalSource, this)) {
IsSpaceKeyDown = false;
if (IsMouseCaptured) {
ReleaseMouseCapture();
}
OnClick();
e.Handled = true;
}
} else {
// On any other key we set IsPressed to false only if
// Space key is pressed
if (IsSpaceKeyDown) {
IsSpaceKeyDown = false;
if (IsMouseCaptured) {
ReleaseMouseCapture();
}
}
}
}
private bool IsSpaceKeyDown
{
get {
return ReadControlFlag(ControlBoolFlags.IsSpaceKeyDown);
}
set {
WriteControlFlag(ControlBoolFlags.IsSpaceKeyDown, value);
}
}
#endregion
#region "Command"
/// <summary>
/// The DependencyProperty for RoutedCommand
/// </summary>
[CommonDependencyProperty()]
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(MyButton),
new FrameworkPropertyMetadata(
(ICommand)null,
new PropertyChangedCallback(
OnCommandChanged)));
/// <summary>
/// Get or set the Command property
/// </summary>
[Bindable(true), Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public ICommand Command
{
get {
return (ICommand)GetValue(CommandProperty);
}
set {
SetValue(CommandProperty, value);
}
}
private static void OnCommandChanged(
DependencyObject valTarget,
DependencyPropertyChangedEventArgs e)
{
MyButton locMyButton = valTarget as MyButton;
if (locMyButton != null) {
locMyButton.OnCommandChanged(
(ICommand)e.OldValue,
(ICommand)e.NewValue);
}
}
private void OnCommandChanged(
ICommand valOldCommand,
ICommand valNewCommand)
{
if (valOldCommand != null) {
valOldCommand.CanExecuteChanged -= OnCanExecuteChanged;
}
if (valNewCommand != null) {
valNewCommand.CanExecuteChanged += OnCanExecuteChanged;
}
UpdateCanExecute();
}
#endregion
#region "CommandParameter"
/// <summary>
/// The DependencyProperty for the CommandParameter
/// </summary>
[CommonDependencyProperty()]
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
"CommandParameter",
typeof(object),
typeof(MyButton),
new FrameworkPropertyMetadata(
(object)null));
/// <summary>
/// Reflects the parameter to pass to the CommandProperty
/// upon execution.
/// </summary>
[Bindable(true), Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public object CommandParameter
{
get {
return GetValue(CommandParameterProperty);
}
set {
SetValue(CommandParameterProperty, value);
}
}
#endregion
#region "CommandTarget"
/// <summary>
/// The DependencyProperty for Target property
/// Flags: None
/// Default Value: null
/// </summary>
[CommonDependencyProperty()]
public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register(
"CommandTarget",
typeof(IInputElement),
typeof(MyButton),
new FrameworkPropertyMetadata(
(IInputElement)null));
/// <summary>
/// The target element on which to fire the command.
/// </summary>
[Bindable(true), Category("Action")]
public IInputElement CommandTarget
{
get {
return (IInputElement)GetValue(CommandTargetProperty);
}
set {
SetValue(CommandTargetProperty, value);
}
}
#endregion
#region "CanExecute"
private void OnCanExecuteChanged(object valTarget, EventArgs e)
{
if (valTarget != null) {
UpdateCanExecute();
}
}
private bool CanExecute
{
get {
return !ReadControlFlag(ControlBoolFlags.CommandDisabled);
}
set {
if (value != CanExecute) {
WriteControlFlag(
ControlBoolFlags.CommandDisabled,
!value);
CoerceValue(IsEnabledProperty);
}
}
}
private void UpdateCanExecute()
{
if (Command != null) {
CanExecute = CanExecuteCommandSource(this);
} else {
CanExecute = true;
}
}
#endregion
#region "IsDefault"
/// <summary>
/// The DependencyProperty for the IsDefault property.
/// Flags: None
/// Default Value: false
/// </summary>
public static readonly DependencyProperty IsDefaultProperty =
DependencyProperty.RegisterAttached(
"IsDefault",
typeof(bool),
typeof(MyButton),
new UIPropertyMetadata(
false,
new PropertyChangedCallback(
OnIsDefaultChanged)));
/// <summary>
/// Specifies whether or not this button is the default button.
/// </summary>
/// <value></value>
public bool IsDefault
{
get {
return (bool)GetValue(IsDefaultProperty);
}
set {
SetValue(IsDefaultProperty, value);
}
}
private static void OnIsDefaultChanged(
DependencyObject valTarget,
DependencyPropertyChangedEventArgs e)
{
MyButton locMyButton = valTarget as MyButton;
if (locMyButton != null) {
Window locWindow = Window.GetWindow(locMyButton);
if (locWindow == null) {
locWindow = Application.Current.MainWindow;
}
if (FocusChangedEventHandler == null) {
FocusChangedEventHandler =
new KeyboardFocusChangedEventHandler(
locMyButton.OnFocusChanged);
}
if (locWindow != null) {
if ((bool)e.NewValue) {
AccessKeyManager.Register("\x000D", locMyButton);
KeyboardNavigation.SetAcceptsReturn(
locMyButton, true);
locMyButton.UpdateIsDefaulted(
Keyboard.FocusedElement);
} else {
AccessKeyManager.Unregister("\x000D", locMyButton);
KeyboardNavigation.SetAcceptsReturn(
locMyButton, false);
locMyButton.UpdateIsDefaulted(null);
}
}
}
}
private static KeyboardFocusChangedEventHandler FocusChangedEventHandler;
private void OnFocusChanged(object valTarget, KeyboardFocusChangedEventArgs e)
{
UpdateIsDefaulted(Keyboard.FocusedElement);
}
#endregion
#region "IsDefaulted"
/// <summary>
/// The key needed set a read-only property.
/// </summary>
private static readonly DependencyPropertyKey IsDefaultedPropertyKey =
DependencyProperty.RegisterReadOnly(
"IsDefaulted",
typeof(bool),
typeof(MyButton),
new FrameworkPropertyMetadata(
false));
/// <summary>
/// The DependencyProperty for the IsDefaulted property.
/// Flags: None
/// Default Value: false
/// </summary>
public static readonly DependencyProperty IsDefaultedProperty =
IsDefaultedPropertyKey.DependencyProperty;
/// <summary>
/// Specifies whether or not this button is the button that
/// would be invoked when Enter is pressed.
/// </summary>
/// <value></value>
public bool IsDefaulted
{
get {
return (bool)GetValue(IsDefaultedProperty);
}
}
private void UpdateIsDefaulted(IInputElement valFocusElement)
{
// If it's not a default button, or nothing is focused,
// or it's disabled
// then it's not defaulted.
if (!IsDefault || valFocusElement == null || !IsEnabled) {
SetValue(IsDefaultedPropertyKey, false);
return;
}
DependencyObject locFocusDependencyObj =
valFocusElement as DependencyObject;
object locThisScope = null;
object locFocusScope = null;
// If the focused thing is not in this scope then
// IsDefaulted = false
AccessKeyPressedEventArgs locEventArgs =
default(AccessKeyPressedEventArgs);
bool locIsDefaulted = false;
try {
// Step 1: Determine the AccessKey scope from currently
// focused element
locEventArgs = new AccessKeyPressedEventArgs();
valFocusElement.RaiseEvent(locEventArgs);
locFocusScope = locEventArgs.Scope;
// Step 2: Determine the AccessKey scope from this button
locEventArgs = new AccessKeyPressedEventArgs();
this.RaiseEvent(locEventArgs);
locThisScope = locEventArgs.Scope;
// Step 3: Compare scopes
if (object.ReferenceEquals(locThisScope, locFocusScope)
&& (locFocusDependencyObj == null
|| !(bool)locFocusDependencyObj.GetValue(KeyboardNavigation.AcceptsReturnProperty))) {
locIsDefaulted = true;
}
} finally {
SetValue(IsDefaultedPropertyKey, locIsDefaulted);
}
}
#endregion
#region "IsCancel"
/// <summary>
/// The DependencyProperty for the IsCancel property.
/// Flags: None
/// Default Value: false
/// </summary>
public static readonly DependencyProperty IsCancelProperty =
DependencyProperty.Register(
"IsCancel",
typeof(bool),
typeof(MyButton),
new FrameworkPropertyMetadata(
false,
new PropertyChangedCallback(
OnIsCancelChanged)));
/// <summary>
/// Specifies whether or not this button is the cancel button.
/// </summary>
/// <value></value>
public bool IsCancel
{
get {
return (bool)GetValue(IsCancelProperty);
}
set {
SetValue(IsCancelProperty, value);
}
}
private static void OnIsCancelChanged(
DependencyObject valTarget,
DependencyPropertyChangedEventArgs e)
{
MyButton locMyButton = valTarget as MyButton;
if (locMyButton != null) {
if ((bool)e.NewValue) {
AccessKeyManager.Register("\x001B", locMyButton);
} else {
AccessKeyManager.Unregister("\x001B", locMyButton);
}
}
}
#endregion
#region "Helper Functions"
/// <summary>
/// This allows a caller to override its ICommandSource values
//// (used by Button and ScrollBar)
/// </summary>
static internal void ExecuteCommand(
ICommand command,
object parameter,
IInputElement target)
{
RoutedCommand routed = command as RoutedCommand;
if (routed != null) {
if (routed.CanExecute(parameter, target)) {
routed.Execute(parameter, target);
}
} else if (command.CanExecute(parameter)) {
command.Execute(parameter);
}
}
static internal bool CanExecuteCommandSource(
ICommandSource commandSource)
{
ICommand command = commandSource.Command;
if (command != null) {
object parameter = commandSource.CommandParameter;
IInputElement target = commandSource.CommandTarget;
RoutedCommand routed = command as RoutedCommand;
if (routed != null) {
if (target == null) {
target = commandSource as IInputElement;
}
return routed.CanExecute(parameter, target);
} else {
return command.CanExecute(parameter);
}
}
return false;
}
/// <summary>
/// Executes the command on the given command source.
/// </summary>
/// <SecurityNote>
/// Critical - calls critical function (ExecuteCommandSource).
/// TreatAsSafe - always passes in false for userInitiated,
//// which is safe
/// </SecurityNote>
[SecurityCritical(), SecuritySafeCritical()]
static internal void ExecuteCommandSource(
ICommandSource commandSource)
{
CriticalExecuteCommandSource(commandSource, false);
}
/// <summary>
/// Executes the command on the given command source.
/// </summary>
/// <SecurityNote>
/// Critical - sets the user initiated bit on a command,
/// which is used for security purposes later.
/// It is important to validate the callers of this,
/// and the implementation to make sure
/// that we only call MarkAsUserInitiated in the
/// correct cases.
/// </SecurityNote>
[SecurityCritical()]
static internal void CriticalExecuteCommandSource(
ICommandSource commandSource,
bool userInitiated)
{
ICommand command = commandSource.Command;
if (command != null) {
object parameter = commandSource.CommandParameter;
IInputElement target = commandSource.CommandTarget;
RoutedCommand routed = command as RoutedCommand;
if (routed != null) {
if (target == null) {
target = commandSource as IInputElement;
}
if (routed.CanExecute(parameter, target)) {
routed.Execute(parameter, target);
}
} else if (command.CanExecute(parameter)) {
command.Execute(parameter);
}
}
}
/// <summary>
/// DialogCancel Command. It closes window if it's dialog and return
/// false as the dialog value.
/// </summary>
/// <remarks>
/// Right now this is only used by Cancel Button to close the dialog.
static internal readonly RoutedCommand DialogCancelCommand =
new RoutedCommand(
"DialogCancel",
typeof(Window));
#endregion
#region "ControlFlags"
internal bool ReadControlFlag(ControlBoolFlags reqFlag)
{
return (_ControlBoolField & reqFlag) != 0;
}
internal void WriteControlFlag(ControlBoolFlags reqFlag, bool @set)
{
if (@set)
{
_ControlBoolField = _ControlBoolField | reqFlag;
}
else
{
_ControlBoolField = _ControlBoolField & (~reqFlag);
}
}
internal enum ControlBoolFlags : ushort
{
ContentIsNotLogical = 0x1,
// used in contentcontrol.cs
IsSpaceKeyDown = 0x2,
// used in ButtonBase.cs
HeaderIsNotLogical = 0x4,
// used in HeaderedContentControl.cs, HeaderedItemsControl.cs
CommandDisabled = 0x8,
// used in ButtonBase.cs, MenuItem.cs
ContentIsItem = 0x10,
// used in contentcontrol.cs
HeaderIsItem = 0x20,
// used in HeaderedContentControl.cs, HeaderedItemsControl.cs
ScrollHostValid = 0x40,
// used in ItemsControl.cs
ContainsSelection = 0x80,
// used in TreeViewItem.cs
VisualStateChangeSuspended = 0x100
// used in Control.cs
}
#endregion
}
/// <summary>
/// An attribute that indicates that a DependencyProperty
/// declaration is common
/// enough to be included in KnownTypes.cs.
/// </summary>
[Conditional("COMMONDPS")]
internal sealed class CommonDependencyPropertyAttribute : Attribute
{
}