从视图模型中设置WPF文本框的焦点

149

我在视图中有一个TextBox和一个Button

现在当用户点击按钮时,我会检查一个条件,如果条件为false,则向用户显示消息,然后我必须设置光标到TextBox控件。

if (companyref == null)
{
    var cs = new Lipper.Nelson.AdminClient.Main.Views.ContactPanels.CompanyAssociation(); 

    MessageBox.Show("Company does not exist.", "Error", MessageBoxButton.OK,
                    MessageBoxImage.Exclamation);

    cs.txtCompanyID.Focusable = true;

    System.Windows.Input.Keyboard.Focus(cs.txtCompanyID);
}
上面的代码在ViewModel中。
“CompanyAssociation”是视图名称。
但是光标没有设置在TextBox中。
XAML如下:
<igEditors:XamTextEditor Name="txtCompanyID" 
                         KeyDown="xamTextEditorAllowOnlyNumeric_KeyDown"
                         ValueChanged="txtCompanyID_ValueChanged"
                         Text="{Binding Company.CompanyId,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"
                         Width="{Binding ActualWidth, ElementName=border}"
                         Grid.Column="1" Grid.Row="0"
                         VerticalAlignment="Top"
                         HorizontalAlignment="Stretch"
                         Margin="0,5,0,0"
                         IsEnabled="{Binding Path=IsEditable}"/>

<Button Template="{StaticResource buttonTemp1}"
        Command="{Binding ContactCommand}"
        CommandParameter="searchCompany"
        Content="Search"
        Width="80"
        Grid.Row="0" Grid.Column="2"
        VerticalAlignment="Top"
        Margin="0"
        HorizontalAlignment="Left"
        IsEnabled="{Binding Path=IsEditable}"/>

当您正在使用Caliburn.Micro时,这个 是一个非常好的解决方案。 - matze8426
21个回答

301

让我分三个部分回答你的问题。

  1. I'm wondering what is "cs.txtCompanyID" in your example? Is it a TextBox control? If yes, then you are on a wrong way. Generally speaking it's not a good idea to have any reference to UI in your ViewModel. You can ask "Why?" but this is another question to post on Stackoverflow :).

  2. The best way to track down issues with Focus is... debugging .Net source code. No kidding. It saved me a lot of time many times. To enable .net source code debugging refer to Shawn Bruke's blog.

  3. Finally, general approach that I use to set focus from ViewModel is Attached Properties. I wrote very simple attached property, which can be set on any UIElement. And it can be bound to ViewModel's property "IsFocused" for example. Here it is:

     public static class FocusExtension
     {
         public static bool GetIsFocused(DependencyObject obj)
         {
             return (bool) obj.GetValue(IsFocusedProperty);
         }
    
         public static void SetIsFocused(DependencyObject obj, bool value)
         {
             obj.SetValue(IsFocusedProperty, value);
         }
    
         public static readonly DependencyProperty IsFocusedProperty =
             DependencyProperty.RegisterAttached(
                 "IsFocused", typeof (bool), typeof (FocusExtension),
                 new UIPropertyMetadata(false, OnIsFocusedPropertyChanged));
    
         private static void OnIsFocusedPropertyChanged(
             DependencyObject d, 
             DependencyPropertyChangedEventArgs e)
         {
             var uie = (UIElement) d;
             if ((bool) e.NewValue)
             {
                 uie.Focus(); // Don't care about false values.
             }
         }
     }
    

    Now in your View (in XAML) you can bind this property to your ViewModel:

     <TextBox local:FocusExtension.IsFocused="{Binding IsUserNameFocused}" />
    
如果这个答案没有帮助到您,请参考第二个答案。

9
好的想法。我需要将IsUserNameFocused设置为true,然后再设置为false才能使其正常工作,这样对吗? - Sam
23
如果您希望您的控件除了逻辑焦点外还能接收键盘焦点,那么在OnIsFocusedPropertyChanged事件中,您还应该调用Keyboard.Focus(uie) - Rachel
9
这该怎么使用呢?如果我将我的属性设置为true,控件就会获得焦点。但是当我回到这个视图时,它总是会再次获得焦点。从OnIsFocusedPropertyChanged中重置它并不能改变这一点。直接在ViewModel中设置后重置它不再使任何东西获得焦点。这行不通。那70个点赞者到底做了什么? - ygoe
3
您可以将obj.SetValue(IsFocusedProperty, value);更改为obj.SetValue(IsFocusedProperty, false);,无需再设置false和true。 - Owen Johnson
5
我还将回调函数改为了这样: ...if ((bool)e.NewValue && uie.Dispatcher != null) { uie.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)(() => uie.Focus())); // 如果你已经附加了一些其他处理程序到UIE GotFocus,那么使用Invoke会更好。 uie.SetValue(IsFocusedProperty, false); // 尽可能重置绑定值,以允许再次设置 ...有时候,如果我要多次设置焦点,甚至需要在ViewModel中将'IsFocused'重置为false。但是这样做可以解决其他方法无法解决的问题。 - Simon D.
显示剩余9条评论

89

我知道这个问题已经被解答了无数次,但我对Anvaka的回答进行了一些修改,我认为这有助于那些像我一样遇到类似问题的人。

首先,我将上面的附加属性更改如下:

public static class FocusExtension
{
    public static readonly DependencyProperty IsFocusedProperty = 
        DependencyProperty.RegisterAttached("IsFocused", typeof(bool?), typeof(FocusExtension), new FrameworkPropertyMetadata(IsFocusedChanged){BindsTwoWayByDefault = true});

    public static bool? GetIsFocused(DependencyObject element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        return (bool?)element.GetValue(IsFocusedProperty);
    }

    public static void SetIsFocused(DependencyObject element, bool? value)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        element.SetValue(IsFocusedProperty, value);
    }

    private static void IsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var fe = (FrameworkElement)d;

        if (e.OldValue == null)
        {
            fe.GotFocus += FrameworkElement_GotFocus;
            fe.LostFocus += FrameworkElement_LostFocus;
        }

        if (!fe.IsVisible)
        {
            fe.IsVisibleChanged += new DependencyPropertyChangedEventHandler(fe_IsVisibleChanged);
        }

        if (e.NewValue != null && (bool)e.NewValue)
        {
            fe.Focus();
        }
    }

    private static void fe_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var fe = (FrameworkElement)sender;
        if (fe.IsVisible && (bool)fe.GetValue(IsFocusedProperty))
        {
            fe.IsVisibleChanged -= fe_IsVisibleChanged;
            fe.Focus();
        }
    }

    private static void FrameworkElement_GotFocus(object sender, RoutedEventArgs e)
    {
        ((FrameworkElement)sender).SetValue(IsFocusedProperty, true);
    }

    private static void FrameworkElement_LostFocus(object sender, RoutedEventArgs e)
    {
        ((FrameworkElement)sender).SetValue(IsFocusedProperty, false);
    }
}

我添加可见性引用的原因是标签页。显然,如果您在最初可见的选项卡之外使用附加属性,则必须手动将控件聚焦才能使附加属性起作用。

另一个障碍是在失去焦点时以更优雅的方式重置底层属性为false。那就是失去焦点事件的作用。

<TextBox            
    Text="{Binding Description}"
    FocusExtension.IsFocused="{Binding IsFocused}"/>

如果有更好的处理可见性问题的方法,请告诉我。

注意:感谢Apfelkuacha提出在DependencyProperty中添加BindsTwoWayByDefault的建议。我很久以前就在自己的代码中实现了这个功能,但从未更新过这篇帖子。由于这个改变,WPF代码中不再需要Mode=TwoWay。


11
除了我需要在GotFocus / LostFocus中添加一个“if (e.Source == e.OriginalSource)”检查以避免在我的UserControl上使用时产生堆栈溢出(字面意思),这对我很有效。因为我的UserControl确实将焦点重定向到内部组件。我删除了Visible检查,接受它的工作方式类似于.Focus()方法。如果.Focus()不起作用,则绑定也不应该工作-这对我的情况来说是可以接受的。 - HelloSam
1
我正在使用WF 4.5。在IsFocusedChanged事件中,我有一个场景(一个Activity被重新加载),其中e.NewValue为null并引发异常,因此首先要检查它。通过这个小改变,一切都运行良好。 - Olaru Mircea
1
谢谢,这很好用 :) 我只是在 'FrameworkPropertyMetadata' 中添加了 ' {BindsTwoWayByDefault = true}',将默认模式设置为双向绑定,因此不需要在每个绑定中都加上它。 - R00st3r
1
我知道这是一个旧答案,但我遇到了一个情况,我想将焦点转移到的控件的IsEnabled属性与多值转换器相关联。 显然,GotFocus事件处理程序在多值转换器之前被调用...这意味着该控件在那时是禁用的,因此一旦GotFocus完成,LostFocus就会被调用(我猜是因为该控件仍然被禁用)。 有什么想法如何处理它? - Mark Olbert
1
@MarkOlbert 使用 fe.Dispatcher.BeginInvoke(new Action(() => { fe.Focus(); }), DispatcherPriority.Loaded); 以确保在加载后更新。更多信息请参见:https://www.telerik.com/forums/isfocused-property#OXgFYZFOg0WZ2rxidln61Q - Coden
显示剩余5条评论

35

我认为保持MVVM原则的清晰最佳方式是使用MVVM Light提供的Messenger类,以下是如何使用它:

在你的视图模型(例如exampleViewModel.cs)中写入以下代码:

 Messenger.Default.Send<string>("focus", "DoFocus");

现在在您的View.cs文件中(不是XAML文件,而是view.xaml.cs文件)在构造函数中编写以下内容:

 public MyView()
        {
            InitializeComponent();

            Messenger.Default.Register<string>(this, "DoFocus", doFocus);
        }
        public void doFocus(string msg)
        {
            if (msg == "focus")
                this.txtcode.Focus();
        }

这种方法很好用,代码更简洁,并且符合MVVM的标准。


9
如果你想保持MVVM原则的清晰,首先就不应该在你的代码后台编写代码。我认为使用附加属性的方法更加清晰简洁,也不会在你的视图模型中引入大量的魔法字符串。 - Ε Г И І И О
40
厄尔尼诺现象:你从何处得到了这个观点,认为在视图代码后台不应该有任何内容?与UI相关的所有内容都应该在视图的代码后台中。设置UI元素的焦点应该绝对在视图的代码后台中完成。让视图模型决定何时发送消息;让视图决定如何处理消息。*这就是M-V-VM的作用:将数据模型、业务逻辑和UI分离。 - Kyle Hale
根据这个建议,我实现了自己的ViewCommandManager来处理在连接的视图中调用命令。它基本上是常规命令的另一个方向,用于那些ViewModel需要在其View(s)中执行某些操作的情况。它使用反射和弱引用来避免内存泄漏。http://dev.unclassified.de/source/viewcommand(也在CodeProject上) - ygoe
我想要一个Silverlight的?我们能用它吗? - Bigeyes
尽管了解如何在视图模型中使用依赖注入是很好的,但是我认为发送消息(这个解决方案)更直接和容易。 - scsfdev
显示剩余3条评论

17

这些方法都不能完全解决我的问题,但为了帮助其他人,根据一些已经提供的代码,下面是我最终编写的代码。

使用方法如下:

<TextBox ... h:FocusBehavior.IsFocused="True"/>

实现如下:

/// <summary>
/// Behavior allowing to put focus on element from the view model in a MVVM implementation.
/// </summary>
public static class FocusBehavior
{
    #region Dependency Properties
    /// <summary>
    /// <c>IsFocused</c> dependency property.
    /// </summary>
    public static readonly DependencyProperty IsFocusedProperty =
        DependencyProperty.RegisterAttached("IsFocused", typeof(bool?),
            typeof(FocusBehavior), new FrameworkPropertyMetadata(IsFocusedChanged));
    /// <summary>
    /// Gets the <c>IsFocused</c> property value.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns>Value of the <c>IsFocused</c> property or <c>null</c> if not set.</returns>
    public static bool? GetIsFocused(DependencyObject element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        return (bool?)element.GetValue(IsFocusedProperty);
    }
    /// <summary>
    /// Sets the <c>IsFocused</c> property value.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">The value.</param>
    public static void SetIsFocused(DependencyObject element, bool? value)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        element.SetValue(IsFocusedProperty, value);
    }
    #endregion Dependency Properties

    #region Event Handlers
    /// <summary>
    /// Determines whether the value of the dependency property <c>IsFocused</c> has change.
    /// </summary>
    /// <param name="d">The dependency object.</param>
    /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
    private static void IsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // Ensure it is a FrameworkElement instance.
        var fe = d as FrameworkElement;
        if (fe != null && e.OldValue == null && e.NewValue != null && (bool)e.NewValue)
        {
            // Attach to the Loaded event to set the focus there. If we do it here it will
            // be overridden by the view rendering the framework element.
            fe.Loaded += FrameworkElementLoaded;
        }
    }
    /// <summary>
    /// Sets the focus when the framework element is loaded and ready to receive input.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
    private static void FrameworkElementLoaded(object sender, RoutedEventArgs e)
    {
        // Ensure it is a FrameworkElement instance.
        var fe = sender as FrameworkElement;
        if (fe != null)
        {
            // Remove the event handler registration.
            fe.Loaded -= FrameworkElementLoaded;
            // Set the focus to the given framework element.
            fe.Focus();
            // Determine if it is a text box like element.
            var tb = fe as TextBoxBase;
            if (tb != null)
            {
                // Select all text to be ready for replacement.
                tb.SelectAll();
            }
        }
    }
    #endregion Event Handlers
}

17

这是一个旧的线程,但似乎没有一个含有代码的答案来解决Anavanka的被接受答案中存在的问题:如果您在ViewModel中将属性设置为false,则它不起作用;或者如果您将属性设置为true,然后用户手动单击其他内容,然后再次将其设置为true。我无法使Zamotic的解决方案在这些情况下可靠地工作。

综合上面的讨论,我列出了下面的代码,我认为这些代码可以解决这些问题:

public static class FocusExtension
{
    public static bool GetIsFocused(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsFocusedProperty);
    }

    public static void SetIsFocused(DependencyObject obj, bool value)
    {
        obj.SetValue(IsFocusedProperty, value);
    }

    public static readonly DependencyProperty IsFocusedProperty =
        DependencyProperty.RegisterAttached(
         "IsFocused", typeof(bool), typeof(FocusExtension),
         new UIPropertyMetadata(false, null, OnCoerceValue));

    private static object OnCoerceValue(DependencyObject d, object baseValue)
    {
        if ((bool)baseValue)
            ((UIElement)d).Focus();
        else if (((UIElement) d).IsFocused)
            Keyboard.ClearFocus();
        return ((bool)baseValue);
    }
}

话虽如此,对于可以在代码后端完成一行代码的事情来说,这仍然很复杂,并且CoerceValue并不是真正意义上用于这种方式,因此也许代码后台是正确的选择。


2
这个方法一直有效,而被接受的答案则不行。谢谢! - NathanAldenSr

5

在我的情况下,FocusExtension直到我更改了OnIsFocusedPropertyChanged方法才能正常工作。原来的方法只在调试时停止了进程时才能工作。在运行时,进程太快了,什么都没有发生。通过这个小修改和我们的朋友Task的帮助,在两种情况下都可以很好地工作。

private static void OnIsFocusedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var uie = (UIElement)d;
  if ((bool)e.NewValue)
  {
    var action = new Action(() => uie.Dispatcher.BeginInvoke((Action)(() => uie.Focus())));
    Task.Factory.StartNew(action);
  }
}

3
问题在于一旦IsUserNameFocused被设置为true,它就永远不会变成false。通过处理FrameworkElement的GotFocus和LostFocus来解决这个问题。
我在源代码格式方面遇到了问题,所以这里提供一个链接

1
我把 "object fe = (FrameworkElement)d;" 改成了 "FrameworkElement fe = (FrameworkElement)d;" 这样智能提示就可以正常工作了。 - user755404
仍然没有解决问题。每次回到它那个元素时,它仍然保持聚焦状态。 - ygoe

3

Anvaka的优秀代码适用于Windows桌面应用程序。如果你和我一样需要相同的解决方案用于Windows商店应用程序,那么这段代码可能会很有用:

public static class FocusExtension
{
    public static bool GetIsFocused(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsFocusedProperty);
    }


    public static void SetIsFocused(DependencyObject obj, bool value)
    {
        obj.SetValue(IsFocusedProperty, value);
    }


    public static readonly DependencyProperty IsFocusedProperty =
        DependencyProperty.RegisterAttached(
         "IsFocused", typeof(bool), typeof(FocusExtension),
         new PropertyMetadata(false, OnIsFocusedPropertyChanged));


    private static void OnIsFocusedPropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        if ((bool)e.NewValue)
        {
            var uie = d as Windows.UI.Xaml.Controls.Control;

            if( uie != null )
            {
                uie.Focus(FocusState.Programmatic);
            }
        }
    }
}

3
基于@Sheridan的答案,这里提供了一种替代方法(链接),它可以获取和还原WPF键盘焦点。
 <TextBox Text="{Binding SomeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <TextBox.Style>
            <Style>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding SomeTextIsFocused, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Value="True">
                        <Setter Property="FocusManager.FocusedElement" Value="{Binding RelativeSource={RelativeSource Self}}" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBox.Style>
    </TextBox>

在您的视图模型中,按照通常的方式设置绑定,然后将SomeTextIsFocused设置为True,以将焦点设置在您的文本框上。

1
对于那些尝试使用Anvaka上面的解决方案的人来说,我在绑定方面遇到了问题,只有第一次工作时失去焦点才会更新属性为false。你可以手动将属性设置为false然后再设置为true,但更好的解决方案可能是在你的属性中做类似这样的操作:
bool _isFocused = false;
public bool IsFocused 
{
    get { return _isFocused ; }
    set
    {
        _isFocused = false;
        _isFocused = value;
        base.OnPropertyChanged("IsFocused ");
    }
}

这样你只需要将它设置为true,它就能获得焦点。


为什么要使用if语句?_isFocused一旦设置为false,只会在下一行被更改为value。 - Damien McGivern
1
@Tyrsius 你可以通过让依赖属性强制执行来解决这个问题,参见这里- http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/d79f8b34-e116-45f7-a592-42d6b849a437/ - RichardOD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接