komizo
komizo

Reputation: 1061

How to ensure, ViewModel property is bound already on view before changing it value again?

There is following case: ViewModel has an object which changes very fast. (via different threads)

View gets informed via NotifyPropertyChanged interface but it seems it works to slow and before View bind new value and draw it then it changes more times therefore It misses some values.

I also tried to bind View to queue then ViewModel could Enqueue it and View could draw via dequeueing.

Unfortunately another problem occurred: after RaisePropertyChanged(() => queue); View is not informed that it was changed.

In such case the implementation of the INotifyPropertyChanged interface did not worked.

Do you have any idea?

Example code of the ViewModel:

public class ExamplaryViewModel
{
    public ExamplaryViewModel()
    {
        Messenger.Default.Register<NotificationMessage<Message>>(this, m => ProcessNotificationMessage(m.Content));
    }    

    public void ProcessNotificationMessage(Message message)
    {   
        MessageOftenBeingChanged = message;
        RaisePropertyChanged(() => MessageOftenBeingChanged );
    }
}

View binds to MessageOftenBeingChanged.

Another option would be to prepare snapshot as was suggested in comments:

public void ProcessNotificationMessage(Message message)
{
    Messages.Enqueue(message);
    RaisePropertyChanged(() => Messages);
}

View:

<controls:RichTextBoxMonitor Messages="{Binding Messages} 

Control:

public class BindableRichTextBox : RichTextBox
{

    public static readonly DependencyProperty MessagesProperty = DependencyProperty.Register("Messages",
     typeof(ConcurrentQueue<Message>), typeof(BindableRichTextBox ), new FrameworkPropertyMetadata(null, OnQueueChangedChanged));


    public ConcurrentQueue<Message> CyclicMessages
    {
        get { return (ConcurrentQueue<Message>)GetValue(MessagesProperty ); }

        set { SetValue(MessagesProperty , value); }

but then, unfortunately the RaisePropertyChanged() method does not trigger that changes happened.

I planned in control in event OnQueueChangedChanged try dequeueing and just draw items as new Inlines for Paragraph.

Upvotes: 5

Views: 710

Answers (2)

komizo
komizo

Reputation: 1061

Through many investigations I decided that binding to RichText box via any decorator, custom control with additional DependencyProperty and Converter is not efficient.

My conclusion shows that it is worthless to build custom richTextbox - and ensure somehow that new value is shown before it is changed.

I resign direct Binding.

I gather any new messages in buffer - queue.

I decided to use something similar to Consumer (as Wojciech Kulik suggested)

I based my consumer on TimeDispatcher which in its interval on Tick checks if any new messages exists in queue. If true then it dequeues and collects it and finally RaiseMonitorItemsAdd.

View handles above event that way:

  if (dataContext is IMonitorable)
        {
            Context = dataContext as IMonitorable;

            Context.MonitorViewModel.RaiseMonitorItemAdd += MonitorViewModelOnRaiseMonitorItemAdd;
            Context.MonitorViewModel.RaiseMonitorCleared += MonitorViewModel_RaiseMonitorCleared;
        }



    private void MonitorViewModelOnRaiseMonitorItemAdd(object sender, MonitorEventArgs monitorEventArgs)
    {
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
        {
            _paragraph.Inlines.AddRange(MonitorItemConverter.ConvertToInlines(monitorEventArgs.MonitorItem));
            _richTextBox.ScrollToEnd();
        }));
    }

MoreOver in case RichTextBox gains to many items I dump whole log to file.

Upvotes: 0

Wojciech Kulik
Wojciech Kulik

Reputation: 8440

You could implement Producer-Consumer.

Look at this simplified version.

  • RunProducer is only for tests, in your case ProcessNotificationMessage will work in a similar way.
  • RunConsumer is a method which constantly checks for new messages and sets Message with some delay, otherwise a user wouldn't be able to read it.
  • It's just a quick proof of concept, but you could implement it better, for example by providing a methods ShowNextMessage and IsMessageAvailable, then the view could decide when is ready to display a new message and request for it. It would be a better design. Even a user could hide some messages faster then, you'd need only to bind ShowNextMessage to Click event.
  • Full source code

    public class MyViewModel : INotifyPropertyChanged
    {
        public ConcurrentQueue<string> Queue { get; set; }
    
        #region Message
    
        private string _message;
    
        public string Message
        {
            get
            {
                return _message;
            }
            set
            {
                if (_message != value)
                {
                    _message = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion
    
        public MyViewModel()
        {
            Queue = new ConcurrentQueue<string>();
            RunProducer();
            RunConsumer();
        }
    
        public void RunProducer()
        {
            Task.Run(() =>
            {
                int i = 0;
                while (true)
                {
                    if (Queue.Count < 10)
                        Queue.Enqueue("TestTest " + (i++).ToString());
                    else
                        Task.Delay(500).Wait();
                }
            });
        }
    
        public void RunConsumer()
        {
            Task.Run(() =>
            {
                while (true)
                {
                    if (Queue.Count > 0)
                    {
                        string msg = "";
                        if (Queue.TryDequeue(out msg))
                            Message = msg;
                    }
                    else
                    {
                        Task.Delay(500).Wait();
                    }
    
                    Task.Delay(100).Wait();
                }
            });
        }
    
        #region INotifyPropertyChanged
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        public void OnPropertyChanged([CallerMemberName]string propertyName = null)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        #endregion
    }
    

In case of empty queue you could use ManualResetMonitor to avoid unnecessary iterations.

Remarks to your code:
If a collection can be changed then for binding purpose you should use only ObservableCollection<T> (or something that implements INotifyCollectionChanged), because it tracks changes and doesn't reload everything.

However in your code a whole binding should be refreshed (as you notified that whole collection has been changed), but I think this mechanism is smarter and checks if references are equal, if so then no refresh occurs. Probably a hax to set it to null and back would refresh it :-).

Upvotes: 1

Related Questions