How to share a binding value between multiple views? - c#

I am new to WPF and MVVM (coming in from WinForms and Events), so please bear with me!
I am trying to figure out how to use the same INotifyPropertyChanged value binding between multiple views. I am using MVVM Light. I have ViewModels that inherit from ViewModelBase backing my Views (with no code behind). I'm not sure how to explain the issue, but I think an example will make it clear what I'm after.
I have a main window. It has a standard TabControl. On the Login TabItem I have a custom login control. Below the TabControl, I have a custom status bar control. The desired behavior is that when the user logs in, the status bar is updated with their login status and name, and the other TabItems on the main window become enabled (they should be disabled when not logged in).
So to summarize I have:
MainWindow (view) with MainWindowViewModel
Login (view) with LoginViewModel (in TabControl of MainWindow)
StatusBar (view) with StatusBarViewModel (at bottom of MainWindow)
Here is what my StatusBarViewModel looks like:
public class StatusBarViewModel : ViewModelBase, IStatusBarViewModel
{
private bool _isLoggedIn;
public bool IsLoggedIn
{
get { return _isLoggedIn; }
set { Set(ref _isLoggedIn, value); RaisePropertyChanged(); }
}
// other properties follow
}
I inject (using Ninject) the (singleton) concrete instance of IStatusBarViewModel into the LoginViewModel via constructor injection:
public class LoginViewModel : ViewModelBase
{
private IStatusBarViewModel _statusBar;
public LoginViewModel(IStatusBarViewModel statusBar)
{
_statusBar = statusBar;
}
}
And I do the same for the MainWindowViewModel:
public class MainWindowViewModel : ViewModelBase
{
private IStatusBarViewModel _statusBar;
public bool IsLoggedIn => _statusBar.IsLoggedIn;
public MainWindowViewModel(IStatusBarViewModel statusBar)
{
_statusBar = statusBar;
}
}
Note: I think this is where my problem is... Not sure if MVVM Light interprets this as a bindable property and applies the proper change notifications. If I add a setter (which I don't need here), that won't work because A property or indexer may not be passed as an out or ref parameter. So I'm unclear on what is going on when I do this.
Back on track here, so when the login is successful, I am able to update the IsLoggedIn property from the LoginViewModel like so:
_statusBar.IsLoggedIn = true;
I set up the binding in my MainWindow XAML like so:
<TabItem Header="Event" IsEnabled="{Binding IsLoggedIn}">
<views:Events/>
</TabItem>
The binding works correctly when the view is first loaded, but subsequent changes to the property don't trigger a change in IsEnabled. The StatusBar (view) however does update accordingly.
I had tossed around the idea of injecting references to both the StatusBarViewModel and the MainWindowViewModel in to my LoginViewModel (and then having to set two properties after login), but that made me think that I'm not approaching this correctly because I'm creating dependencies.
So basically the question is:
Is my approach correct, per the MVVM pattern?
Am I on the right track and just need to modify the code a bit?
If not, what is the (or a) standard pattern to handle this scenario?

Your guess is correct. The problem is here:
public bool IsLoggedIn => _statusBar.IsLoggedIn;
... because it's not going to generate the change notification. What you could do is just expose the IStatusBarViewModel via a public property and then bind to its own IsLoggedIn property directly.
In the ViewModel:
public class MainWindowViewModel : ViewModelBase
{
private IStatusBarViewModel _statusBar;
public IStatusBarViewModel StatusBar => _statusBar;
public MainWindowViewModel(IStatusBarViewModel statusBar)
{
_statusBar = statusBar;
}
}
And in the View:
<TabItem Header="Event" IsEnabled="{Binding StatusBar.IsLoggedIn}">
<views:Events/>
</TabItem>

Related

WPF Commands---How to attach to ViewModel properly from View for logic?

I have seen quite a few different posts regarding this, but am still confused as to what the proper way to do this is. I am implementing RelayCommand and IRequireViewID to do things like call a WindowsManager class to close windows from the view where it doesn't need to know what window it is calling, etc.
However, situations that arise like this is where I am unsure how to implement things properly.
So basically I have a command in the viewModel where you click a button and I need to save data to a DB once this happens. How do I have the command in the View but then put the logic for this into the ViewModel? IE, basically the ViewModel would need to know when the Command is called. I mean I could always put a static method in the viewmodel and call it from the view but I am assuming that probably is not a good way to implement it, ie MyViewModel.RedValidation from the view.
RelayCommand Class
public class RelayCommand : ICommand
{
private Action commandTask;
public RelayCommand(Action commandToRun)
{
commandTask = commandToRun;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
commandTask();
}
}
View
public ICommand ValidateRed
{
get => new RelayCommand(RedValidation);
}
private void RedValidation()
{
//SAVE Data To DB <----This should be in the ViewModel though right?
}
XAML:
<Button Name="ValidateBtn" Style="{StaticResource mainButtons}" Content="Validate Email" Width="100"
HorizontalAlignment="Left" Command="{Binding ValidateRed}"/>
Everything you have in "View" should be in "ViewModel". Commands are properties of the view model, not the view (also, you shouldn't be binding against the view 99% of the time).
Once you have changed that, it should fall into place the way you expect.

Implementing interfaces in View and ViewModels using MEF and MVVM

Background : I have a Window having a tab control with each tab having a separate UserControl. I have followed MVVM for each user control and MEF to obtain the controls to be displayed in the tab at runtime. This is my implementation
interface ITabControl
{
}
[Export(typeof(UserControl1ViewModel))]
class UserControl1ViewModel
{
}
class UserControl1: ITabControl
{
[Import(typeof(UserControl1ViewModel))]
public UserControl1ViewModel ViewModel
{
get { return this.DataContext as UserControl1ViewModel; }
set { this.DataContext = value; }
}
}
//Other user controls have similar implementation
public class WindowViewModel
{
//Import all type of ITabControl and set the TabCollection(bound to ItemSource property of tab control)
}
Problem : Now I have some validations to be done on a particular set of tabs based on the user action in the main window. So I have used another interface called IConfiguration which is implemented by some user control ViewModels.
interface IConfiguration
{
public void Action1();
public void Action2();
------------------- (many more)
}
public class Window
{
//Import all type of IConfiguration and call Action1/Action2 for all these types based on user actions.
}
Now, if an error is encountered during validation (IConfigure actions impelemented in different ViewModels) in any of the above tabs, I need to set the SelectedTabItem property of the tab control to that particular tab. Since these actions are implemeted in the ViewModel, I'm unable to obtain the UserControl to set the SelectedTabItem property. How do I achieve this?
PS: I know I can achieve this by implementing IConfiguration in UserControl view instead of ViewModel this way
public class UserControl1 : IConfiguration
{
public void Action1
{
this.ViewModel.Action1();
}
public void Action2
{
this.ViewModel.Action2();
}
//--------
}
I wonder if there is a better way to achieve this.
Use an overarching viewmodel which contains a collection of ViewModels (one per tab) and a property which represents the active tab.
When you need to swap the active tab you can do it in the viewmodel just by updating the property that represents the active tab. This answer here shows you how to bind the active tab in the TabControl.

Control the view in MVVM

I created a project with the MVVM model, and done so with the view-first approach.
I have a TextBox in my XAML code, along with a Button to pass the data from the TextBox:
<!-- View - XAML code -->
<TextBox
MinWidth="30"
Name="TagId"/>
<Button
Command="{Binding AddTagCommand}"
CommandParameter="{Binding Text, ElementName=TagId}"
Content="Add"/>
When I click the button, I want the TextBox cleared.
According to the Prism manual:
In some cases, the code-behind may contain UI logic code that implements visual behavior that is difficult or inefficient to express in Extensible Application Markup Language (XAML), such as complex animations, or when the code needs to directly manipulate visual elements that are part of the view.
Here's the code behind, and the viewmodel.
//View - code behind
public partial class ApplicationStarterView : UserControl
{
public ApplicationStarterView()
{
}
public ApplicationStarterView(ApplicationStarterViewModel viewModel) : this()
{
DataContext = viewModel;
InitializeComponent();
}
}
//View model
public class ApplicationStarterViewModel : BindableBase
{
private readonly IUnityContainer _container;
public ApplicationStarterViewModel(IUnityContainer container)
{
_container = container;
AddTagCommand = new DelegateCommand<object>(AddTag);
}
public ICommand AddTagCommand { get; private set; }
private void AddTag(object input)
{
//Forward stuff
//Clear TextBox
}
}
Can I in any way squeeze in some code to do a TagId.Clear()?
I'd bind the text to another property on the view model.
That way, you can skip the command parameter and the AddTagCommand can directly read the new Text property, do his adding stuff and then clear it, thus updating TagId.
Completely unrelated piece of advice: it is almost never a good idea to inject the IUnityContainer... if you need to create stuff, use factories.

Binding to Template10 Settings from Main Page

I am loving Template10 so far, very nice.
I am a little stuck though on how to bind to a Setting value on the Main Page.
I have added a new bool setting which is storing properly.
On my main page I have a Visibility binding to the setting:
Visibility="{Binding UseAmbientLightSensor, Converter={StaticResource CollapsedWhenFalseConverter}}"
This works on app start as expected, the MainPageViewModel reads the value from Settings and a grid is visible or collapsed based on that setting.
However I cannot seem to get this binding to 'listen' to the setting, if I go to the settings page and change that value, when I go back to the Main Page the visibility does not change. It only works if I restart the app.
In the vanilla Template10 install this would be akin to Binding a little logo on MainPage to the 'UseLightThemeButton' setting in the Settings page which changes based on that setting..
Okay, so I guess this is the "official" answer. But many approaches are valid. This one matches most closely to the templates. I would do it like this:
public class MainPageViewModel : ViewModelBase
{
Services.SettingService.SettingService _SettingService;
public MainPageViewModel()
{
_SettingService = Services.SettingService.SettingService.Instance;
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> state)
{
Windows.Storage.ApplicationData.Current.DataChanged += SettingsChanged;
await Task.CompletedTask;
}
public override async Task OnNavigatedFromAsync(IDictionary<string, object> pageState, bool suspending)
{
Windows.Storage.ApplicationData.Current.DataChanged -= SettingsChanged;
await Task.CompletedTask;
}
private void SettingsChanged(Windows.Storage.ApplicationData sender, object args)
{
RaisePropertyChanged(nameof(FontSize));
}
public double FontSize { get { return _SettingService.FontSize; } }
}
With that view-model, you can easily bind to a setting (in this case FontSize).
Best of luck.
There are two possible scenarios that may not be happening:
Raise the property change event when your bool value gets updated.
Set the binding to a two way mode.
In order to do this change the binding mode of your Visibility property
Visibility="{Binding UseAmbientLightSensor, Mode=TwoWay, Converter={StaticResource CollapsedWhenFalseConverter}}"
This will tell xaml to listen to any change on the property in the view model.
Then you need to tell the View model when to let the XAML view know of its changes, if you are using Template10, then it can be done as follows:
private bool useAmbientLightSensor;
public TodoListControlViewModel UseAmbientLightSensor
{
get
{
return this.useAmbientLightSensor;
}
set
{
this.Set(ref this.useAmbientLightSensor, value);
}
}
The view model needs to extend from the ViewModelBase class which provides the Set method that raises the OnPropertyChanged event, allowing the view to know of any change in the view model.
For more info, check the INotifyPropertyChanged interface and its implementation.

Can Pass View to ViewModel in MVVM?

For my project I need to know which View is using my ViewModel
So i created this ViewModel:
public class HistoriqueViewModel : INotifyPropertyChanged
{
public HistoriqueViewModel(MetroWindow view)
{
this.MetroWindow = view;
this.ExportCommand = new RelayCommand(Export_Ex);
}
private MetroWindow _metroWindow;
public MetroWindow MetroWindow
{
get { return _metroWindow; }
set
{
if (Equals(value, _metroWindow)) return;
_metroWindow = value;
OnPropertyChanged();
}
}
//.........
}
And in the View constructor:
public partial class ViewHisto : MetroWindow
{
public ViewHisto()
{
InitializeComponent();
DataContext=new HistoriqueMV(this) ;
}
}
It Work perfectly for me but I want to know if this Break the MVVM Pattern?
Yes, this breaks MVVM. A properly constructed view model shouldn't care about what the view is.
Nothing in your code really suggests why you are passing that reference (other than exposing the view as a public property, which is an even bigger no-no) but there are several ways around it:
Pass the view as an interface and hold/expose that
Use a mediator to pass whatever messages necessary between the view model/view
Have the view invoke whatever methods it needs on the view model, and have the view model raise events that the view can listen to.
Any of the above approaches will provide far better decoupling than the one you are going with.
One other thing, its "View Model", not "Model View"

Categories