简体   繁体   中英

How can I make sure I'm in sync with the UI for style changes based on binding, before modifying the binded property in my C# WPF app?

I'm making an MVVM app that shows a list that grows over time, and marks the new elements not listed when the list was previously updated. I'm trying to make sure I use async methods so the GUI feels responsive and doesn't lock.

I bind to the list item's ResultDate and the LastCheckedTime in the viewmodel, send those through a converter, style accordingly if ResultDate is newer then LastCheckedTime:

<DataGridTextColumn.ElementStyle>
    <Style TargetType="{x:Type TextBlock}">
        <Style.Triggers>
            <DataTrigger Value="True">
                <DataTrigger.Binding>
                    <MultiBinding Converter="{StaticResource CompareIfNewerConverter}">
                        <Binding Path="ResultDate" />
                        <Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.LastCheckedTime"/>
                    </MultiBinding>
                </DataTrigger.Binding>
                <Setter Property="Foreground" Value="#FFF90E0E"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>
</DataGridTextColumn.ElementStyle>

In my ViewModel, I run an async SQL request, which manually fires PropertyChanged for LastCheckedTime after results come, so the UI marks the new items accordingly, and only then updates LastCheckedTime , and doesn't fire the PropertyChanged (which is to be fired only next time). However, if I don't add a delay, the styles are done after LastCheckedTime . I don't like to depend on timing, because one cannot know whether it will break on somebody else's computer. Here's my hack to fix the problem - this is working on my end:

private async Task DoTheFilter(CancellationToken token)
{
    MatchingResultList = await Connectivity.ReturnResultsForPanicViewAsync(token);

    DataGridHeader = (MatchingResultList != null) ? $"Records found: {MatchingResultList.Count}" : "Cancelled.";

    if(MatchingResultList != null && MatchingResultList.Any((r) => r.ResultDate > LastCheckedTime)) System.Media.SystemSounds.Exclamation.Play(); //If any result is newer then the last listing time, warn user with sound

    OnPropertyChanged("LastCheckedTime"); //update visual indicator for new results manually.
    await Task.Delay(133); //allow GUI redraw - Timing based and prone to error?
    LastCheckedTime = DateTime.Now; //update the last checked time only then.
}

If I don't add a delay, the styles are applied after the LastTimeChecked is updated in the ViewModel, which means no new item is shown as new. I've seen that even a delay of 1 milliseconds fixes the issue, but I made 133 just to pad it. However, I'd really like to know if there's a fully robust and dependable fix, or whether there's an entirely different approach that I may take.

Edit: I solved the problem using two ways - here's the first:

OnPropertyChanged("LastCheckedTime"); //update visual indicator for new results manually.

Application.Current.Dispatcher.Invoke(new Action(() => LastCheckedTime = DateTime.Now), DispatcherPriority.ContextIdle, null); //only update LastCheckedTime after context becomes idle.

After I manually notify the ui of the last change, I update the LastCheckedTime only after the context is idle. This seemed to be a more robust way compared to waiting an arbitrary amount of time.

However, the better solution seems to be 'buffering' the LastCheckedTime using a second DateTime as suggested by Peter Duniho (solution #3). Here's the code... The command is modified so LastCheckedTime is updated first before calling DoTheFilter()

FilterTestCommand = AsyncCommand.Create((token) => { LastCheckedTime = NewCheckedTime; return DoTheFilter(token); });

And I update NewCheckedTime inside DoTheFilter() . I turned on automatic notifications for the LastCheckedTime property change, btw. That said, this may force an unnecessary re-update of the current list before the new list arrives, but it doesn't pose a performance problem. The solution is effective yet simple, that it's somewhat embarrassing for me to not have thought of it, but I did learn some new tricks (dispatcher) that I may make use of elsewhere.

I'd really like to know if there's a fully robust and dependable fix, or whether there's an entirely different approach that I may take

Yes, there is. You are correct to be suspicious of your current approach. It's inherently broken and will eventually produce incorrect results. Your implementation appears to rely on an assumption that WPF will never on its own recheck the LastCheckedTime value, and do so only when you call the OnPropertyChanged() method directly. It's true that it probably won't, but there's no contractual requirement for it to refrain from doing so, and I can easily imagine a scenario where WPF decides it needs to refresh all its bindings for some reason and winds up retrieving the latest value for the LastCheckedTime even though you didn't explicitly tell it to.

The only reliable way to do this is to ensure you have reliable input for the visual display in the first place. You can accomplish this in one of at least three ways I can think of:

  1. Add a bool property to the view model indicating whether the item is new. Before you retrieve the current data, clear the flag on all your items. When you retrieve the data, instead of rewriting the entire set of items, just add the new ones, setting the "is item new" flag on those new items. Instead of multi-binding with a comparison converter, just bind directly to this flag property.
  2. As a variation on the previous idea, you can still rewrite the entire set of items, but before you update the LastCheckedTime value, go through the list of items and set the flag appropriately according to whether that item's "result" time is more recent than the LastCheckedTime value or not. Again, bind to the flag property instead of using the comparison converter.
  3. In addition to the "last-checked" time, store also a "previous-checked" time. Use the "previous-checked" time for your comparison in the view. The only thing you'll use the "last-checked" time for is to update the "previous-checked" time each time you retrieve your data. In this case, you can still use the multi-binding with the comparison converter; it's just that you'll compare with the "previous-checked" time value instead of "last-checked".

I note that, based on your description, it sounds as though the LastCheckedTime property in your window object's model (ie the DataContext object) does not automatically raise a property-changed notification when the property value is changed. This is in and of itself suspect. If you use one of the techniques above, you will be able to go ahead and implement property-changed notifications on all model properties, in each property setter. And if you do that, you will ensure that all bindings will update immediately when any bound property changes, which will in turn ensure consistent and bug-free behavior from the code, without any timing-related problems for the user to witness.


If the above seems a bit vague, it's only because your question does not include a good Minimal, Complete, and Verifiable example that reliably reproduces the problem. If you'd like more specific advice, including a good code example showing exactly what the above options look like, please add a good code example to the question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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