简体   繁体   中英

What is missing in this update UI via dispatcher/databinding

I have a simple WPF window with: Loaded="StartTest" and

<Grid>
        <ListBox ItemsSource="{Binding Logging, IsAsync=True}"></ListBox>
</Grid>

In code behind I have in method StartTest :

LogModel LogModel = new LogModel();

void StartTest(object sender, RoutedEventArgs e)
{
    DataContext = LogModel;

    for (int i = 1; i<= 10; i++)
    {
       LogModel.Add("Test");
       Thread.Sleep(100);
    }
}

And class LogModel is:

public class LogModel : INotifyPropertyChanged
{
    public LogModel()
    {
        Dispatcher = Dispatcher.CurrentDispatcher;
        Logging = new ObservableCollection<string>();
    }
    Dispatcher Dispatcher;

    public ObservableCollection<string> Logging { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    public void Add(string text)
    {
        Dispatcher.BeginInvoke((Action)delegate ()
        {
            Logging.Add(text);
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Logging"));
        });
    }
}

Of course the problem is that the UI doesn't update in the loop. What am I missing?
How can I achieve the UI update?

ObservableCollection already raises the PropertyChanged event when it's modified. You don't have to raise the event in the UI thread either.

Your model can be as simple as :

class LogModel
{
    public ObservableCollection<string> Logging { get; } = new ObservableCollection<string>();

    public void Add(string text)
    {
        Logging.Add(text);
    }
}

All you need to do is set it as the DataContext of your view, eg :

LogModel model = new LogModel();
public MainWindow()
{
    InitializeComponent();
    this.DataContext = model;
}

I assume StartTest is a click handler which means it runs on the UI thread. That means it will block the UI thread until the loop finishes. Once the loop finishes the UI will be updated.

If you want the UI to remain responsive during the loop, use Task.Delay instead of Thread.Slepp, eg :

private async void Button_Click(object sender, RoutedEventArgs e)
{
    for(int i=0;i<10;i++)
    {
        await Task.Delay(100);
        model.Add("Blah!");
    }
}

Update

You don't need to use an ObservableCollection as a data binding source. You could use any object, including an array or List. In this case though you'd have to raise the PropertyChanged event in code :

class LogModel:INotifyPropertyChanged
{
    public List<string> Logging { get; } = new List<string>();

    public event PropertyChangedEventHandler PropertyChanged;

    public void Add(string text)
    {
        Logging.Add(text);
        PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Logging"));
    }
}

This will tell the view to load all the contents and display them again. This is perfectly fine when you only want to display data loaded eg from the database without modifying them, as it makes mapping entities to ViewModels a lot easier. In this case you only need to update the view when a new ViewModel is attached as a result of a command.

This is not efficient when you need to update the coolection though. ObservableCollection implements the INotifyCollectionChanged interface that raises an event for each change. If you add a new item, only that item will be rendered.

On the other hand you should avoid modifying the collection in tight loops because it will raise multiple events. If you load 50 new items, don't call Add 50 times in a loop. Create a new ObservableCollection, replace the old one and raise the PropertyChanged event, eg :

class LogModel:INotifyPropertyChanged
{
    public ObservableCollection<string> Logging { get; set; } = new ObservableCollection<string>();

    public event PropertyChangedEventHandler PropertyChanged;

    public void Add(string text)
    {            
        Logging.Add(text);
        PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Logging"));
    }

    public void BulkLoad(string[] texts)
    {
        Logging = new ObservableCollection<string>(texts);
        PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Logging"));
    }
}

The explicit implementation is still needed because the Logging property is getting replaced and can't raise any events itself

The reason why the UI is not updated in the loop is a call to Dispatcher.BeginInvoke . This places a new DispatcherOperation in the dispatcher queue. But your loop is already a dispatcher operation, and it continues on the Dispatcher 's thread. So all the operations you queue will be executed after the loop's operation is finished.

Maybe you wanted to run the StartTest on a background thread? Then, the UI will update.

By the way, don't block the Dispatcher 's thread with Thread.Sleep . It prevents the Dispatcher from doing its things as smoothly as possible.

It is the DoEvents thing, overhere :

public static void DoEvents()
{
    Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
                                          new Action(delegate { }));
}

or even the perhaps better https://stackoverflow.com/a/11899439/138078 .

Of course the test should be written differently in a way which does not require it.

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