【问题标题】:WPF Listbox focus from viewmodel来自视图模型的 WPF 列表框焦点
【发布时间】:2019-07-15 06:54:03
【问题描述】:

我偶然发现了 Listbox 和焦点的众所周知的问题。我正在从视图模型中设置ItemsSource,在某些时候我需要重新加载它们并将选择和焦点设置为特定项目,比如:

private readonly ObservableCollection<ItemViewModel> items;
private ItemViewModel selectedItem;

private void Process()
{
    items.Clear();

    for (int i = 0; i < 100; i++)
    {
        items.Add(new ItemViewModel(i));
    }

    var item = items.FirstOrDefault(i => i.Value == 25);
    SelectedItem = item;
}

public ObservableCollection<ItemViewModel> Items { /* usual stuff */ }
public ItemViewModel SelectedItem { /* usual stuff */ }

绑定可能如下所示:

<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" />

调用方法项后被选中,但没有获得焦点。

我在 Internet 和 StackOverflow 上阅读了很多内容,但我找到的所有答案都涉及手动填充列表框,而不是通过视图模型绑定。所以问题是:如何在呈现的场景中正确关注新选择的项目?


为了添加一些上下文,我正在实现一个侧边栏文件浏览器:

我需要在树视图下方的列表框上进行键盘导航。

【问题讨论】:

  • 我认为这可以通过附加到列表框的行为来完成。连接到 SelectedItemChanged 事件并相应地手动检查/设置焦点。除了“可以做”之外,问题仍然是“应该做”。例如,如果你点击一个按钮来触发你的更新,那么任何项目/列表框失去焦点不是很正常吗??
  • @SanchoPanza,看看我的编辑。我已经尝试过了,但是即使之前没有聚焦,列表框也会窃取焦点。在树视图中选择项目后,我随后选择列表框中的第一个元素。这实际上禁止在树视图上导航,因为列表框立即成为焦点......
  • @SanchoPanza,关于应该做,场景是:用户选择“..”并回车;我重新加载文件以显示上层文件夹内容,并且我想选择用户所在的文件夹以改进导航。由于元素被重新加载,没有焦点,如果我没有自己设置焦点,用户最终会在按下向下箭头键后获得焦点并选择第一个项目...
  • @Spook 您是否尝试在ListBox xaml 中设置IsSynchronizedWithCurrentItem
  • @Spook:除非您修改 ListBoxItem 的控件模板,否则必须将 ListBox 聚焦以突出显示该项目。

标签: c# wpf listbox focus


【解决方案1】:

这是一个可能对您有用的解决方案:

控件:

class FocusableListBox : ListBox
{
    #region Dependency Proeprty

    public static readonly DependencyProperty IsFocusedControlProperty = DependencyProperty.Register("IsFocusedControl", typeof(Boolean), typeof(FocusableListBox), new UIPropertyMetadata(false, OnIsFocusedChanged));

    public Boolean IsFocusedControl
    {
        get { return (Boolean)GetValue(IsFocusedControlProperty); }
        set { SetValue(IsFocusedControlProperty, value); }
    }

    public static void OnIsFocusedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        ListBox listBox = dependencyObject as ListBox;
        listBox.Focus();
    }

    #endregion Dependency Proeprty
}

视图模型:

 class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private Boolean _IsFocused;
    private String selectedItem;

    public ObservableCollection<String> Items { get; private set; }
    public String SelectedItem
    {
        get
        {
            return selectedItem;
        }
        set
        {
            selectedItem = value;
            RaisePropertyChanged("SelectedItem");
        }
    }      

    public Boolean IsFocused
    {
        get { return _IsFocused; }
        set
        {
            _IsFocused = value;
            RaisePropertyChanged("IsFocused");
        }
    }

    public ViewModel()
    {
        Items = new ObservableCollection<string>();
        Process();
    }

    private void Process()
    {
        Items.Clear();

        for (int i = 0; i < 100; i++)
        {
            Items.Add(i.ToString());
        }

        ChangeFocusedElement("2");
    }

    public void ChangeFocusedElement(string newElement)
    {
        var item = Items.FirstOrDefault(i => i == newElement);
        IsFocused = false;
        SelectedItem = item;
        IsFocused = true;
    }

    private void RaisePropertyChanged(String propName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
}

XAML:

<local:FocusableListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" 
                            HorizontalAlignment="Left" VerticalAlignment="Stretch"
                            ScrollViewer.VerticalScrollBarVisibility="Auto"
                            Width="200" 
                            IsFocusedControl="{Binding IsFocused, Mode=TwoWay}"/>

更新调用:

 _viewModel.ChangeFocusedElement("10");

【讨论】:

    【解决方案2】:

    我最终在控件的代码隐藏中得到了以下代码:

    public void FixListboxFocus()
    {
        if (lbFiles.SelectedItem != null)
        {
            lbFiles.ScrollIntoView(lbFiles.SelectedItem);
            lbFiles.UpdateLayout();
    
            var item = lbFiles.ItemContainerGenerator.ContainerFromItem(viewModel.SelectedFile);
            if (item != null && item is ListBoxItem listBoxItem && !listBoxItem.IsFocused)
                listBoxItem.Focus();
        }
    }
    

    此方法可用于从 viewModel 中调用,每次设置选择时都会调用它:

    var file = files.FirstOrDefault(f => f.Path.Equals(subfolderName, StringComparison.OrdinalIgnoreCase));
    if (file != null)
        SelectedFile = file;
    else
        SelectedFile = files.FirstOrDefault();
    
    access.FixListboxFocus();
    

    access 是通过接口传递给 ViewModel 的视图(以保持表示和逻辑之间的分离)。相关的 XAML 部分如下所示:

    <ListBox x:Name="lbFiles" ItemsSource="{Binding Files}" SelectedItem="{Binding SelectedFile}" />
    

    【讨论】:

    • 您可以使用 ListBox 的 SelectionChanged 事件,而不是从 VM 调用该方法
    • @NawedNabiZada,不,我不能,因为只有当视图模型中的选择发生变化时,才应该调用此方法。否则,每次选择更改(即重新加载项目)时,列表框都会窃取焦点
    猜你喜欢
    • 2015-05-18
    • 1970-01-01
    • 2011-07-19
    • 1970-01-01
    • 2011-09-06
    • 2010-11-24
    • 1970-01-01
    • 2011-02-20
    • 2012-10-20
    相关资源
    最近更新 更多