enzom83
enzom83

Reputation: 8320

How to update the view after the model has changed?

Say I have a Logger class, a LoggerViewModel class and a MainWindow with a TextBox. The Logger class is a thread-safe singleton, so I have only an instance of it in the application domain.

public sealed class Logger : INotifyPropertyChanged
{
    private static readonly Logger _Instance = new Logger();

    private static readonly object _SyncLock = new object();
    private static List<LogEntry> _Data = new List<LogEntry>();

    /// <summary>
    /// 
    /// </summary>
    private Logger() { ; }

    /// <summary>
    /// 
    /// </summary>
    public static Logger Instance
    {
        get { return _Instance; }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="entry"></param>
    public void Write(LogEntry entry)
    {
        lock (_SyncLock)
        {
            _Data.Add(entry);
        }
        this.RaiseNotifyPropertyChanged("Entries");
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="component"></param>
    /// <param name="message"></param>
    public void Write(string component, string message)
    {
        LogEntry entry = LogEntry.Create(component, message);
        Write(entry);
    }

    /// <summary>
    /// 
    /// </summary>
    public IList<LogEntry> Entries
    {
        get
        {
            lock (_SyncLock)
            {
                return new ReadOnlyCollection<LogEntry>(_Data);
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="property"></param>
    private void RaiseNotifyPropertyChanged(string property)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(property));
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
}

The Logger unique instance is updated by more thread while the application is running, so I would update the TextBox on the MainWindow whenever the model (that is the Logger singleton class) changes.

How to connect the Model and the ViewModel between them? I emphasize that the Model is changed by only a few application thread, so it is read-only from the point of view of UI.

I provided the LoggerText property within the LoggerViewModel class, since I thought the following working mechanism. 1. When the Model (the Logger instance) changes, it notifies the ViewModel. 2. The ViewModel receives the notify by the Model and create a new string containing all the messages from the logger. 3. The ViewModel notifies the View.

public class LoggerViewModel : INotifyPropertyChanged
{
    Logger _LoggerModel;

    /// <summary>
    /// 
    /// </summary>
    public LoggerViewModel()
    {
        _LoggerModel = Logger.Instance;
    }

    /// <summary>
    /// 
    /// </summary>
    public string LoggerText
    {
        get
        {
            string text = "";
            List<LogEntry> entries = new List<LogEntry>(_LoggerModel.Entries);
            foreach (LogEntry entry in entries)
            {
                text += entry.ToString();
                text += "\n";
            }
            return text;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string propertyName)
    {
        // take a copy to prevent thread issues
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

How the ViewModel can intercept the notifications sent by the Model?

Upvotes: 1

Views: 206

Answers (1)

Jakob Christensen
Jakob Christensen

Reputation: 14956

First of all I do not like that you are using a singleton. When using the singleton pattern you are making it hard on yourself when testing or reusing your view controllers. I would instead inject the Logger dependency into your LoggerViewModel class.

Aside from that one way to solve your problem is to register a handler for the PropertyChanged event on your Logger and build the text when the event fires for the Entries property.

In LoggerViewModel you would then add a property handler and update the LoggerText property as needed.

public LoggerViewModel(Logger loggerModel /* Dependency injection*/)
{
    _LoggerModel = loggerModel;
    _LoggerModel.PropertyChanged += this.LoggerModel_PropertyChanged;
}

private void LoggerModel_PropertyChanged(object sender, PropertyChangedEventArgs args)
{
    if (args.PropertyName == "Entries")
    {
        StringBuilder text = new StringBuilder();  // Use StringBuilder for performance
        List<LogEntry> entries = new List<LogEntry>(_LoggerModel.Entries);
        foreach (LogEntry entry in entries)
        {
            text.AppendLine(entry.ToString());
        }
        this.LoggerText = text.ToString();            
    }
}

private string _loggerText;
public string LoggerText
{
    set
    {
       _loggerText = value;
       RaisePropertyChanged("LoggerText");
    }

    get
    {
        return _loggerText;
    }
}

Disclaimer: The above code is written without a compiler.

Upvotes: 1

Related Questions