Reputation: 161
I am putting my first MVVM project together. I have a StatusBar that will be updated from various views (UserControls) from within the application. Each view will have its own DataContext. My original thought was to create a ViewModelBase class which implemented the INotifyPropertyChanged interface and also contained a public property to bind the the text of my StatusBar to. All other ViewModels within the application would then inherit the ViewModelBase class. Of course, this does not work. How can I accomplish this? I am not using MVVM Light or any other frameworks and I am programming in vb.net. Thanks in advance.
Update - Below is the translation of what Garry proposed in the 2nd answer, I am still unable to modify the status text from the MainViewModel?? Anyone see a problem with the vb translation of his c# code? This MVVM transition is causing major hair loss!!
ViewModelBase.vb
Imports System.ComponentModel
Public Class ViewModelBase
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
StatusViewModel.vb
Public Interface IStatusBarViewModel
Property StatusBarText() As String
End Interface
Public Class StatusBarViewModel
Inherits ViewModelBase
Implements IStatusBarViewModel
Private _statusBarText As String
Public Property StatusBarText As String Implements IStatusBarViewModel.StatusBarText
Get
Return _statusBarText
End Get
Set(value As String)
If value <> _statusBarText Then
_statusBarText = value
OnPropertyChanged("StatusBarText")
End If
End Set
End Property
End Class
MainViewModel.vb
Public Class MainViewModel
Inherits ViewModelBase
Private ReadOnly _statusBarViewModel As IStatusBarViewModel
Public Sub New(statusBarViewModel As IStatusBarViewModel)
_statusBarViewModel = statusBarViewModel
_statusBarViewModel.StatusBarText = "Test"
End Sub
End Class
Status.xaml (UserControl)
<StatusBar DataContext="{Binding StatusViewModel}">
...
<w:StdTextBlock Text="{Binding StatusText, UpdateSourceTrigger=PropertyChanged}" />
Application.xaml.vb
Class Application
Protected Overrides Sub OnStartup(e As System.Windows.StartupEventArgs)
Dim iStatusBarViewModel As IStatusBarViewModel = New StatusBarViewModel()
Dim mainViewModel As New MainViewModel(iStatusBarViewModel)
Dim mainWindow As New MainWindow() With { _
.DataContext = mainViewModel _
}
mainWindow.Show()
End Sub
End Class
MainWindow.xaml
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GlobalStatusBarTest"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<local:Status Grid.Row="1" />
</Grid>
</Window>
Upvotes: 4
Views: 4361
Reputation: 1937
You can use Messages. Summary:
Messenger:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//Messenger downloaded from StackOverflow and small modifications made
namespace GuitarDiary.Desktop.Utility
{
public class Messenger
{
private static readonly object CreationLock = new object();
private static readonly ConcurrentDictionary<MessengerKey, object> Dictionary = new ConcurrentDictionary<MessengerKey, object>();
#region Default property
private static Messenger _instance;
/// <summary>
/// Gets the single instance of the Messenger.
/// </summary>
public static Messenger Default
{
get
{
if (_instance == null)
{
lock (CreationLock)
{
if (_instance == null)
{
_instance = new Messenger();
}
}
}
return _instance;
}
}
#endregion
/// <summary>
/// Initializes a new instance of the Messenger class.
/// </summary>
private Messenger()
{
}
/// <summary>
/// Registers a recipient for a type of message T. The action parameter will be executed
/// when a corresponding message is sent.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="recipient"></param>
/// <param name="action"></param>
public void Register<T>(object recipient, Action<T> action)
{
Register(recipient, action, null);
}
/// <summary>
/// Registers a recipient for a type of message T and a matching context. The action parameter will be executed
/// when a corresponding message is sent.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="recipient"></param>
/// <param name="action"></param>
/// <param name="context"></param>
public void Register<T>(object recipient, Action<T> action, object context)
{
var key = new MessengerKey(recipient, context);
Dictionary.TryAdd(key, action);
}
/// <summary>
/// Unregisters a messenger recipient completely. After this method is executed, the recipient will
/// no longer receive any messages.
/// </summary>
/// <param name="recipient"></param>
public void Unregister(object recipient)
{
Unregister(recipient, null);
}
/// <summary>
/// Unregisters a messenger recipient with a matching context completely. After this method is executed, the recipient will
/// no longer receive any messages.
/// </summary>
/// <param name="recipient"></param>
/// <param name="context"></param>
public void Unregister(object recipient, object context)
{
object action;
var key = new MessengerKey(recipient, context);
Dictionary.TryRemove(key, out action);
}
/// <summary>
/// Sends a message to registered recipients. The message will reach all recipients that are
/// registered for this message type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="message"></param>
public void Send<T>(T message)
{
Send(message, null);
}
/// <summary>
/// Sends a message to registered recipients. The message will reach all recipients that are
/// registered for this message type and matching context.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="message"></param>
/// <param name="context"></param>
public void Send<T>(T message, object context)
{
IEnumerable<KeyValuePair<MessengerKey, object>> result;
if (context == null)
{
// Get all recipients where the context is null.
result = from r in Dictionary where r.Key.Context == null select r;
}
else
{
// Get all recipients where the context is matching.
result = from r in Dictionary where r.Key.Context != null && r.Key.Context.Equals(context) select r;
}
foreach (var action in result.Select(x => x.Value).OfType<Action<T>>())
{
// Send the message to all recipients.
action(message);
}
}
protected class MessengerKey
{
public object Recipient { get; private set; }
public object Context { get; private set; }
/// <summary>
/// Initializes a new instance of the MessengerKey class.
/// </summary>
/// <param name="recipient"></param>
/// <param name="context"></param>
public MessengerKey(object recipient, object context)
{
Recipient = recipient;
Context = context;
}
/// <summary>
/// Determines whether the specified MessengerKey is equal to the current MessengerKey.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
protected bool Equals(MessengerKey other)
{
return Equals(Recipient, other.Recipient) && Equals(Context, other.Context);
}
/// <summary>
/// Determines whether the specified MessengerKey is equal to the current MessengerKey.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((MessengerKey)obj);
}
/// <summary>
/// Serves as a hash function for a particular type.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
unchecked
{
return ((Recipient != null ? Recipient.GetHashCode() : 0) * 397) ^ (Context != null ? Context.GetHashCode() : 0);
}
}
}
}
}
StatusBar VM:
public class StatusBarViewModel : INotifyPropertyChanged
{
private string _statusBarText;
public string StatusBarText
{
get => _statusBarText;
set
{
_statusBarText = value;
OnPropertyChanged(nameof(StatusBarText));
}
}
public MainViewModel()
{
Messenger.Default.Register<UpdateStatusBar>(this, OnUpdateStatusBar);
}
private void OnUpdateStatusBar(UpdateStatusBar obj)
{
StatusBarText = obj.Text;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Message class:
public class UpdateStatusBar
{
public UpdateStatusBar(string text)
{
Text = text;
}
public string Text { get; set; }
}
Send message to status bar:
StatusBarService.WriteToStatusBar($"Exercise '{Exercise.Name}' created");
XAML (make sure the view that contains this XAML is using StatusBar VM):
<StatusBar Background="SlateBlue"
DockPanel.Dock="Bottom">
<StatusBarItem Content="{Binding StatusBarText}"></StatusBarItem>
</StatusBar>
Upvotes: 0
Reputation: 8792
To begin, take your main window, the application shell, and change it to something like this...
<DockPanel>
<sample:StatusBarControl DockPanel.Dock="Bottom" x:Name="StatusBarRegion"/>
<sample:MainContentControl DockPanel.Dock="Top" x:Name="MainContentRegion"/>
</DockPanel>
</Grid>
This partitions the shell into regions much like the Prism Region Manager. Note that the 'content' is now a user control, along with any other views you want to place in that region. Note also that the status bar has a region, but will not change its view or view model.
In the code behind of your shell, place two dependency properties like this...
public MainWindow()
{
InitializeComponent();
}
#region MainContentDp (DependencyProperty)
public object MainContentDp
{
get { return GetValue(MainContentDpProperty); }
set { SetValue(MainContentDpProperty, value); }
}
public static readonly DependencyProperty MainContentDpProperty =
DependencyProperty.Register("MainContentDp", typeof(object), typeof(MainWindow),
new PropertyMetadata(OnMainContentDpChanged));
private static void OnMainContentDpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MainWindow mainWindow = d as MainWindow;
if (mainWindow != null)
{
mainWindow.MainContentRegion.DataContext = e.NewValue;
}
}
#endregion
#region StatusBarDp (DependencyProperty)
public object StatusBarDp
{
get { return GetValue(StatusBarDpProperty); }
set { SetValue(StatusBarDpProperty, value); }
}
public static readonly DependencyProperty StatusBarDpProperty =
DependencyProperty.Register("StatusBarDp", typeof(object), typeof(MainWindow),
new PropertyMetadata(OnStatusBarDpChanged));
private static void OnStatusBarDpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MainWindow mainWindow = d as MainWindow;
if (mainWindow != null)
{
mainWindow.StatusBarRegion.DataContext = e.NewValue;
}
}
#endregion
These dependency properties are substitutes for the Prism Region Manager. Each Dependency Property sets the data context of its associated region. In your case, you want to change the MainContentDp to switch back and forth between view models.
In the app.xaml.cs file, you can override the start up method to look like this...
public partial class App
{
protected override void OnStartup(System.Windows.StartupEventArgs e)
{
IStatusBarViewModel iStatusBarViewModel = new StatusBarViewModel();
MainViewModel mainViewModel = new MainViewModel(iStatusBarViewModel);
OtherViewModel otherViewModel = new OtherViewModel(iStatusBarViewModel);
MainWindow mainWindow = new MainWindow
{
StatusBarDp = iStatusBarViewModel,
MainContentDp = mainViewModel
};
mainWindow.Show();
}
}
This code creates the shell and assigns the two Dependency Properties, which in turn will populate the regions with their respective view models.
A view model would look like this...
public class MainViewModel : ViewModelBase
{
public ICommand ClickCommand { get; set; }
private readonly IStatusBarViewModel _statusBarViewModel;
public MainViewModel(IStatusBarViewModel statusBarViewModel)
{
_statusBarViewModel = statusBarViewModel;
ClickCommand = new RelayCommand(ExecuteClickCommand, CanExecuteClickCommand);
}
private void ExecuteClickCommand(object obj)
{
_statusBarViewModel.StatusBarText = "Updating the db";
}
private bool CanExecuteClickCommand(object obj)
{
return true;
}
public void DoSomethingVeryImportant()
{
_statusBarViewModel.StatusBarText = "Starting some work";
// do some work here
_statusBarViewModel.StatusBarText = "Done doing some work";
}
}
And another view model...
public class OtherViewModel : ViewModelBase
{
private readonly IStatusBarViewModel _statusBarViewModel;
public OtherViewModel(IStatusBarViewModel statusBarViewModel)
{
_statusBarViewModel = statusBarViewModel;
}
public void UpdateTheDatabase()
{
_statusBarViewModel.StatusBarText = "Starting db update";
// do some work here
_statusBarViewModel.StatusBarText = "Db update complete";
}
}
}
Both VM's get the same status bar in their constructors and both are writing to the same status bar. Both VM's take turns sharing the same region in your shell, "MainContentRegion". Prism does all of this without the verbosity of this answer, but since you are in VB and do not use Prism, this approach will work fine.
Upvotes: 1
Reputation: 27105
There are several options to achieve this in a nice clean way.
When using Prism or MvvmLight style frameworks, some kind of 'event aggregator' or 'messenger' class can be used to send messages between different parts of the application. Consider a view for the status bar, containing a text block and a progress bar. There would be a view model along the lines of this:
Class StatusViewModel
Public Property Progress() As Integer
...
Public Property Text() As String
...
End Class
Without having to create dependencies from everything to the StatusViewModel, we could use a shared service to send a message containing the information to change the progress or the text. For example
'progress changed to 75%
Messenger.SendMessage(Of ProgressChangedMessage)(New ProgressChangedMessage(75))
The StatusViewModel
can then update its progress based on the inbound messages:
Messenger.[AddHandler](Of ProgressChangedMessage)(Function(message)
Me.Progess = message.NewProgress
End Function)
A second solution would be to share the StatusViewModel with every view model that wants to change the progress or status bar text. Dependency Injection or Service Location makes this easy, but if you want to keep it simple you can use a singleton instance to access the StatusViewModel
.
When you expose the StatusViewModel to other classes it might be wise to use an interface, e.g:
Public Interface IStatusService
Sub SetProgress(progress As Integer)
Function SetText(text As String) As String
End Interface
edit: changed code to vb.
Upvotes: 0