简体   繁体   English

如何单独从ViewModel以编程方式突出显示DataGrid行?

[英]How can I highlight a DataGrid row programmatically from the ViewModel alone?

The SelectedItem property binding is not causing its DataGrid row to be highlighted on initial load. SelectedItem属性绑定不会导致其初始加载时突出显示其DataGrid行。

I have a DataGrid with a binding on SelectedItem that is not getting highlighted until I click it again. 我有一个带有SelectedItem绑定的DataGrid,直到我再次单击它才会突出显示。 I think I have an order of operations problem--coming from the fact that all the ViewModel code runs before the Views get rendered. 我认为我有一个操作顺序问题 - 来自所有ViewModel代码在Views被渲染之前运行的事实。 It works fine once I click a row (even the same one that's already in the SelectedAccount prop), but I need to able to highlight a row from the VM. 一旦我点击一行(即使是已经在SelectedAccount道具中的那一行),它也能正常工作,但我需要能够突出显示VM中的一行。

I can easily verify the SelectedAccount property is not null because there are other ViewModels that display it's values via PubSubEvents. 我可以轻松验证SelectedAccount属性不为null,因为还有其他ViewModel通过PubSubEvents显示它的值。

I've tried several solutions methods, and the only way I've been able to get it to work so far just feels dirty: 我已经尝试了几种解决方法,到目前为止我能够让它工作的唯一方法就是感觉很脏:

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();
    }   
}

I don't like this because it causes the OnPropertyChanged event to fire twice, once before the views load, and and again after the above hack. 我不喜欢这个,因为它导致OnPropertyChanged事件在视图加载之前触发两次,并且在上述hack之后再次触发。 This triggers a SQL call, so I want to avoid that. 这会触发SQL调用,所以我想避免这种情况。 I also thought the point of MVVM was to decouple the view from the viewmodel, but maybe I'm not understanding that as well as I thought. 我还认为MVVM的观点是将视图与视图模型分离,但也许我并不像我想的那样理解它。

Here's the XAML for the DataGrid: 这是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>

And the intial Load method in the ViewModel, which is where I want to set the highlighted row. ViewModel中的初始Load方法,我想要设置突出显示的行。

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

EDIT 编辑

The issue was subtle, but makes perfect sense now. 问题很微妙,但现在很有道理。

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);
}

Raising this event for a private property doesn't make sense, as the view cannot see it, and is bound to SelectedAccount. 为私有属性引发此事件没有意义,因为视图无法看到它,并且绑定到SelectedAccount。 Changing it to OnPropertyChanged("SelectedAccount") did the trick. 将其更改为OnPropertyChanged(“SelectedAccount”)就可以了。

Implementing INotifyPropertyChanged should be enough, this code works on my end, I'm using a Command to call the Load() method but it's probably not needed in your code. 实现INotifyPropertyChanged应该足够了,这段代码在我的最后工作,我使用Command来调用Load()方法,但是你的代码中可能不需要它。

ViewModel and C# code : 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 : 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>

In your view model, use your framework's RaisePropertyChanged(); 在视图模型中,使用框架的RaisePropertyChanged(); function (or whatever the equivalent is). 功能(或等效的任何东西)。 In the code-behind of the DataGrid element, try this code: 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;
        }
    }
}

In my example, I used a generic string list of names, so you may need to alter the line TextBlock cellContent = DataGrid.Columns[0].GetCellContent(row) as TextBlock; 在我的示例中,我使用了一个通用的字符串名称列表,因此您可能需要将TextBlock cellContent = DataGrid.Columns[0].GetCellContent(row) as TextBlock;的行TextBlock cellContent = DataGrid.Columns[0].GetCellContent(row) as TextBlock; and cellContent.Text.Equals(DataGrid.SelectedItem)) to meet your line selection criteria. cellContent.Text.Equals(DataGrid.SelectedItem))以满足您的行选择标准。

The other alternative if you don't want to use the code-behind is an attached behavior that more-or-less does the same thing. 如果您不想使用代码隐藏,另一种选择是附加行为,或多或少做同样的事情。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM