【问题标题】:How can I highlight a DataGrid row programmatically from the ViewModel alone?如何仅从 ViewModel 以编程方式突出显示 DataGrid 行?
【发布时间】:2018-02-19 22:21:37
【问题描述】:

SelectedItem 属性绑定未导致其 DataGrid 行在初始加载时突出显示。

我有一个与 SelectedItem 绑定的 DataGrid,直到我再次单击它才突出显示。我认为我有一个操作顺序问题 - 来自所有 ViewModel 代码在视图渲染之前运行的事实。一旦我点击一行(即使是已经在 SelectedAccount 属性中的同一行),它就可以正常工作,但我需要能够突出显示 VM 中的一行。

我可以轻松验证 SelectedAccount 属性不为空,因为还有其他 ViewModel 通过 PubSubEvents 显示它的值。

我已经尝试了几种解决方法,但到目前为止我能够让它工作的唯一方法就是感觉很脏:

using ApplicationName.UI.ViewModels;
public partial class AccountsView : UserControl
{
    public AccountsView()
    {
        InitializeComponent();
        Loaded += AccountsView_Loaded;
    }

    private void AccountsView_Loaded(object sender, RoutedEventArgs e)
    {
        AccountsViewModel viewModel = (AccountsViewModel)DataContext;
        AccountsDataGrid.SelectedItem = viewModel.SelectedAccount;
        AccountsDataGrid.Focus();
        UpdateLayout();
    }   
}

我不喜欢这样,因为它会导致OnPropertyChanged 事件触发两次,一次是在视图加载之前,一次是在上述 hack 之后。这会触发 SQL 调用,所以我想避免这种情况。我还认为 MVVM 的目的是将视图与视图模型分离,但也许我并没有像我想象的那样理解这一点。

这是 DataGrid 的 XAML:

<ScrollViewer Grid.Row="1"
              Grid.Column="0"
              Grid.ColumnSpan="3"
              Margin="5,0">
    <DataGrid Name="AccountsDataGrid"
              ItemsSource="{Binding Accounts}"
              SelectedItem="{Binding SelectedAccount}"
              AutoGenerateColumns="False"
              CanUserResizeColumns="True"
              SelectionMode="Single"
              SelectionUnit="FullRow">
        <DataGrid.Columns>
            <DataGridTextColumn Header="ClinicId" 
                                TextBlock.TextAlignment="Center"
                                Width="75"
                                Binding="{Binding ClinicId}" />
            <DataGridTextColumn Header="Account#"
                                Width="75"
                                Binding="{Binding AccountNumber}"/>
            <DataGridTextColumn Header="LastName"
                                Width="1*"
                                Binding="{Binding LastName}" />
            <DataGridTextColumn Header="FirstName"
                                Width="1*"
                                Binding="{Binding FirstName}" />
            <DataGridTextColumn Header="Balance"
                                Width="Auto"
                                Binding="{Binding Balance}" />
            <DataGridTextColumn Header="Follow Up"
                                Width="100"
                                Binding="{Binding FollowUpDate}"/>
        </DataGrid.Columns>
    </DataGrid>
</ScrollViewer>

以及 ViewModel 中的初始 Load 方法,这是我要设置突出显示的行的地方。

public void Load()
{
    RefreshGrid();
    SelectedAccount = Accounts.First();
    _accountId = SelectedAccount.Id;
}

编辑

这个问题很微妙,但现在很有意义。

private Account _selectedAccount;

public Account SelectedAccount
{
    get => _selectedAccount;
    set => SetSelectedAccount(value);
}

private void SetSelectedAccount(Account value)
{
    _selectedAccount = value;
    OnPropertyChanged("_selectedAccount");  // <= whoops
    if (_selectedAccount != null)
        OnAccountSelected(
           _selectedAccount.PrimaryKeyFields);
}

为私有属性引发此事件没有意义,因为视图无法看到它,并且绑定到 SelectedAccount。将其更改为 OnPropertyChanged("SelectedAccount") 就可以了。

【问题讨论】:

  • 你确定只调用 .Focus 还不够吗?如果您的 viewModel 正确实现,则 SelectedItem 绑定应该可以工作。您确定不是“InactiveSelectionHighlight”扰乱了您对突出显示的感知。
  • @user6144226 你是对的。我的 ViewModel 中有一个错误。我为_selectedAccount 而不是SelectedAcount 提出OnPropertyChanged。这不起作用的原因现在很明显。我能够从代码隐藏处理程序中删除除 Focus() 之外的所有内容。一切顺利!

标签: c# wpf mvvm data-binding


【解决方案1】:

实现INotifyPropertyChanged 就足够了,这段代码对我来说有效,我正在使用Command 来调用Load() 方法,但您的代码中可能不需要它。

ViewModel 和 C# 代码:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        DataContext = new ViewModel();
    }
}

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ViewModel()
    {
        Accounts = new List<Account>();

        Accounts.AddRange(
            Enumerable.Range(0, 10)
                .Select(r => new Account
                {
                    AccountNumber = r,
                    FirstName = $"First{r}",
                    LastName = $"Last{r}"
                }));

        LoadedCommand = new WpfCommand((param) => Load());
    }

    private void Load()
    {
        SelectedAccount = Accounts.FirstOrDefault(a => a.AccountNumber == 2);
    }

    public WpfCommand LoadedCommand { get; set; }

    public List<Account> Accounts { get; set; }

    private Account _selectedAccount = null;
    public Account SelectedAccount
    {
        get { return _selectedAccount; }
        set
        {
            _selectedAccount = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedAccount)));
        }
    }
}

public class Account
{
    public int AccountNumber { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class WpfCommand : ICommand
{
    private Action<object> _execute;
    private Func<object, bool> _canExecute;

    public event EventHandler CanExecuteChanged;

    public WpfCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public void Execute(object parameter)
    {
        _execute?.Invoke(parameter);
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute?.Invoke(parameter) ?? true;
    }
}

XAML:

   <!--System.Windows.Interactivity.WPF nuget package-->
   xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

    <DataGrid 
        ItemsSource="{Binding Accounts}"
        SelectedItem="{Binding SelectedAccount}">

        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Loaded">
                <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>

        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding AccountNumber}"/>
            <DataGridTextColumn Binding="{Binding LastName}" />
            <DataGridTextColumn Binding="{Binding FirstName}" />
        </DataGrid.Columns>
    </DataGrid>

【讨论】:

  • 感谢您花时间拼写所有这些内容。它帮助我更好地理解大局。有时在一个页面上查看所有移动部分确实更好。原来我是在提高OnPropertyChanged("_selectedAccount") 而不是OnPropertyChanged("SelectedAccount")。视图看不到private 成员... derp!我能够从方法背后的代码中删除除 DataGrid.Focus() 调用之外的所有内容,现在一切都按预期工作。
【解决方案2】:

在您的视图模型中,使用框架的 RaisePropertyChanged(); 函数(或任何等效函数)。在 DataGrid 元素的代码隐藏中,尝试以下代码:

private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{    
    for (int i = 0; i < DataGrid.Items.Count; i++)
    {
        DataGridRow row = (DataGridRow)DataGrid.ItemContainerGenerator.ContainerFromIndex(i);
        TextBlock cellContent = DataGrid.Columns[0].GetCellContent(row) as TextBlock;
        if (cellContent != null && cellContent.Text.Equals(DataGrid.SelectedItem))
        {
            object item = DataGrid.Items[i];
            DataGrid.SelectedItem = item;
            DataGrid.ScrollIntoView(item);
            row.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            break;
        }
    }
}

在我的示例中,我使用了名称的通用字符串列表,因此您可能需要更改 TextBlock cellContent = DataGrid.Columns[0].GetCellContent(row) as TextBlock;cellContent.Text.Equals(DataGrid.SelectedItem)) 行以满足您的行选择标准。

如果您不想使用代码隐藏,另一种选择是附加的行为,它或多或少会做同样的事情。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 2020-11-04
相关资源
最近更新 更多