Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 611
komizo Avatar asked Nov 09 '22 15:11

komizo


1 Answers

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 :-).

like image 75
Wojciech Kulik Avatar answered Nov 15 '22 08:11

Wojciech Kulik