How to notify View from ViewModel without breaking MVVM? - c#

I recently started trying out MVVM pattern in school and was wondering what the best way (if any) is to notify View from the ViewModel, letting the view know to run a method without breaking MVVM? Basically letting the view know if something was successful, like a login attempt or trying to connect to a database?
An example could be a login page, where the mainwindow should change content to a new page only if the login was successful, if not, a messagebox should show up
Edit:
I'm using .NET
What I have tried so far:
View:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:View.Pages" xmlns:ViewModels="clr-namespace:ViewModel.ViewModels;assembly=ViewModel" x:Class="View.Pages.Start_Page"
mc:Ignorable="d"
d:DesignHeight="720" d:DesignWidth="1280"
Title="Start_Page">
<Page.DataContext>
<ViewModels:Start_Page_ViewModel/>
</Page.DataContext>
Code behind it:
public Start_Page()
{
InitializeComponent();
Start_Page_ViewModel currentDataContext = DataContext as Start_Page_ViewModel;
currentDataContext.CurrentUserIDGotten += GoToMenu;
}
private void GoToMenu(int result)
{
if (result == -1)
{
MessageBox.Show("User credentials incorrect");
}
else if (result == -2)
{
MessageBox.Show("Connection failed");
}
else
{
Application.Current.MainWindow.Content = new Menu_Page();
}
}
ViewModel:
public class Start_Page_ViewModel
{
private string userName;
private string userPassword;
public string UserName { get => userName; set => userName = value; }
public string UserPassword { get => userPassword; set => userPassword = value; }
private RelayCommand logIn;
public RelayCommand LogIn => logIn;
public delegate void CurrentUserIDGottenEventHandler(int result);
public event CurrentUserIDGottenEventHandler CurrentUserIDGotten;
public Start_Page_ViewModel()
{
logIn = new RelayCommand(LogInToProgram, CanLogIn);
}
public void LogInToProgram(object o)
{
PasswordBox passwordBox = o as PasswordBox;
ViewModelController.Instance.CurrentUserID = Database_Controller.Instance.SignIn(userName, passwordBox.Password);
OnUserIDGotten(ViewModelController.Instance.CurrentUserID);
}
public bool CanLogIn(object o)
{
if (userName != null)
{
return true;
}
return false;
}
protected virtual void OnUserIDGotten(int result)
{
if (CurrentUserIDGotten != null)
{
CurrentUserIDGotten(result);
}
}
}

In pure way, without specified framework.
Create a event delegate (or listener interface), associate with view model
Register the event handler on view
Fire the event when view model is changed
Likes this
using System;
public class MainClass {
public static void Main (string[] args) {
ViewModel m = new ViewModel();
View v = new View();
v.Model = m;
m.MakeSomeChange();
}
}
public class View {
private IViewModel _model;
public IViewModel Model {
get {
return _model;
}
set {
if(_model != null) {
_model.OnChanged -= OnChanged;
}
if(value != null) {
value.OnChanged += OnChanged;
}
_model = value;
}
}
private void OnChanged(){
//update view
Console.WriteLine ("View Updated");
}
}
public delegate void ViewChangeDelegate();
public interface IViewModel {
event ViewChangeDelegate OnChanged;
}
public class ViewModel: IViewModel {
public event ViewChangeDelegate OnChanged;
public void MakeSomeChange() {
//make some change in the view Model
OnChanged.Invoke();
}
}

Generally, the ViewModel communicates with the View via databindings. The ViewModel might expose a property, like LoginSuccessful, that the the View would bind to. Then, when the property updates, the View would receive a PropertyChanged notification and change some aspect of its appearance. How it would do this varies; for example, a text property in XAML could be bound directly to an underlying ViewModel property:
<TextBox Text="{Binding Source={StaticResource UserViewModel}, Path=Username}"/>
The ViewModel might look like:
public class UserViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
public string Username {
get { return _username; }
set {
_username = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Username"));
}
}
private string _username;
public UserViewModel() { }
}
Whenever the Username property is changed on the UserViewModel class, the text box will update to display the new value.
However, this approach doesn't work for all situations. When working with boolean values, it's often useful to implement data triggers:
<TextBox Text="{Binding Source={StaticResource UserViewModel}, Path=Username}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Source={StaticResource UserViewModel}, Path=IsTaken}" Value="True">
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBox>
This code extends the previous example to color the background of the text box red if the IsTaken property is set to true on the ViewModel. A nice thing about data triggers is that they reset themselves; for example, if the value is set to false the background will revert to its original color.
If you want to go the other way, and notify the ViewModel of user input or a similarly important event, you can use commands. Commands can be bound to properties in XAML, and are implemented by the ViewModel. They are called when the user performs a certain action, such a clicking a button. Commands can be created by implementing the ICommand interface.

Related

Dotnet Maui DataTrigger not fired on Custom Control Binding

I have created a custom control that is a ValidatableEntry. It has an IsValid public property (bool).
I would like to use this property to Enable/Disable a Button. For this, I think I should be able to use a DataTrigger. However it is not working. The Trigger does not fire when the IsValid property changes.
Here is a simplified version that ilustrates the problem. When the entered text is over 5 characters long, the IsValid property changes to true. However, the trigger is not fired and the button remains disabled.
An example repo can be found here: https://github.com/jokogarcia/ExampleForSO
Custom control:
public class ValidatableEntry : ContentView
{
public Entry Entry { get; set; } = new();
public int MinimumLength { get; set; }
public bool IsValid { get; set; }
public ValidatableEntry()
{
this.Entry.TextChanged += OnTextChanged;
Content = new VerticalStackLayout
{
Children = {
Entry
}
};
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
Entry entry = sender as Entry;
IsValid = entry?.Text?.Length> MinimumLength;
}
}
XAML:
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<local:ValidatableEntry
x:Name="MyEntry"
MinimumLength="5"/>
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
IsEnabled="False"
HorizontalOptions="Center" >
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Source={x:Reference MyEntry},
Path=IsValid}"
Value="True">
<Setter Property="IsEnabled" Value="True"></Setter>
</DataTrigger>
</Button.Triggers>
</Button>
</VerticalStackLayout>
I found my own answer. I'll share it here for others that come after.
What I was missing was to implement INotifyPropertyChanged in my Custom Control. Like this:
public class ValidatableEntry : ContentView, INotifyPropertyChanged
{
[...]
public bool IsValid
{
get { return isValid; }
set
{
isValid = value;
NotifyPropertyChanged();
}
}
[...]
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
EDIT: Actually, this approach later gave me problems when using DataBindings on my control. It is actually not necessary to implement INotifyPropertyChanged because ContentView already implements it. All I needed to do is call OnPropertyChanged() after updating the value.
So the better and simpler answer would be:
public class ValidatableEntry : ContentView
{
[...]
public bool IsValid
{
get { return isValid; }
set
{
isValid = value;
OnPropertyChanged();
}
}

Change MainWindow Content by ValueConverter

i am new to wpf and xaml and try to change the content of a window (Login -> Main content and main content -> Login) in an WindowsApplication (Xaml, WPF). So far i have the following for this simple login/logout scenario:
BaseViewModel
public class BaseViewModel : DependencyObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
BaseMainViewViewModel (Base class for setting the MainViewType Property in the MainWindow. It also contains the command to change the property via the button in the MainViews.)
public class BaseMainViewViewModel : BaseViewModel
{
private static MainViewType _CurrentMainView;
private ICommand _SwitchMainViewCommand;
public BaseMainViewViewModel()
{
SwitchMainViewCommand = new RelayCommand(SwitchMainView);
}
public MainViewType CurrentMainView
{
get { return _CurrentMainView; }
set
{
if (value != _CurrentMainView)
{
_CurrentMainView = value;
OnPropertyChanged(nameof(CurrentMainView));
}
}
}
public ICommand SwitchMainViewCommand
{
get { return _SwitchMainViewCommand; }
set { _SwitchMainViewCommand = value; }
}
#region Test
public void SwitchMainView(object param)
{
Debugger.Break();
switch (CurrentMainView)
{
case MainViewType.Login:
CurrentMainView = MainViewType.Main;
break;
case MainViewType.Main:
CurrentMainView = MainViewType.Login;
break;
default:
break;
}
MessageBox.Show("Login/Logout");
}
#endregion Test
LoginViewModel inherites from BaseMainViewViewModel to get access to the CurrentMainView-Property
public class LoginViewModel : BaseMainViewViewModel {}
MainViewModel her the same
public class MainViewModel : BaseMainViewViewModel {}
MainWindowViewModel
public class MainWindowViewModel: BaseMainViewViewModel {}
LoginMainView
public partial class LoginMainView : UserControl
{
public LoginMainView()
{
InitializeComponent();
DataContext = new LoginViewModel();
}
}
Currently i have only one button (Login-Button) in the LoginMainView. If I click this button, the current LoginMainView should be exchanged with the MainMainView.
<Grid>
<Button Content="Main" Background="Red" Command="{Binding SwitchMainViewCommand}" />
</Grid>
MainMainView
public partial class MainMainView : UserControl
{
public LoginMainView()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
here the same (Logout-Button) correspond to LoginMainView...
<Grid>
<Button Content="Logout" Background="Green" Command="{Binding SwitchMainViewCommand}" />
</Grid>
MainWindow
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
In the MainWindow-View i bind the CurrentMainView-Property (MainViewType) from the BaseMainViewViewModel to the contentpresenter, which i will change by clicking the button in the MainMainView/LoginMainView and the ValueConverter shold do the rest.
<Grid>
<StackPanel>
<Label Content="Test" />
<ContentPresenter Content="{Binding CurrentMainView, Converter={view:MainViewValueConverter}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
MainViewType
public enum MainViewType
{
Login = 0,
Main = 1
}
BaseValueConverter
public abstract class BaseValueConverter<T> : MarkupExtension, IValueConverter
where T : class, new()
{
private static T _Converter = null;
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _Converter ?? (_Converter = new T());
}
public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
public abstract object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
RelayCommand
public class RelayCommand : ICommand
{
private Action<object> _Execute;
private Predicate<object> _CanExecute;
private event EventHandler CanExecuteChangedInternal;
public RelayCommand(Action<object> execute) : this(execute, DefaultCanExecute) { }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_Execute = execute ?? throw new ArgumentNullException("execute");
_CanExecute = canExecute ?? throw new ArgumentNullException("canExecute");
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
CanExecuteChangedInternal += value;
}
remove
{
CommandManager.RequerySuggested -= value;
CanExecuteChangedInternal -= value;
}
}
public bool CanExecute(object parameter)
{
return (_CanExecute != null) && _CanExecute(parameter);
}
public void Execute(object parameter)
{
_Execute(parameter);
}
public void OnCanExecuteChanged()
{
EventHandler eventHandler = CanExecuteChangedInternal;
if (eventHandler != null)
{
eventHandler.Invoke(this, EventArgs.Empty);
}
}
public void Destroy()
{
_CanExecute = _ => false;
_Execute = _ => { return; };
}
private static bool DefaultCanExecute(object parameter)
{
return true;
}
}
When i start the application, the ValueConverter is called and the correct View (LoginMainView) is loaded. I then click on the button in the LoginMainView, the command (SwitchMainView) is executed, but then the content of MainWindow is not changed into MainMainView because the ValueConverter is not used.
What am i doing wrong? Do i have a fundamental understanding problem? Or is it not possible in this way to map the simple login/logout scenario? Or did i simply overlook something? Can someone please tell me what i have forgotten?
Many thanks in advance to the helpers!
You don't need ValueConverter for that. You are on a right track thoug. Take a look here - this is sample application for ReactiveUI framework (which is my favourite).
It has AppBootrsapper (ViewModel of the application). As the framework does some magick around it, the basic idea is:
MainWindow.Xaml:
<Window x:Class="ReactiveUI.Samples.Routing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rx="clr-namespace:ReactiveUI;assembly=ReactiveUI"
Title="MainWindow" Height="350" Width="525">
<Grid UseLayoutRounding="True" >
<ContentControl Content="{Binding ActiveViewModel}">
<ContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type LoginViewModel}">
<!-- here you put your content wof login screen, prefereably as seperate UserControl -->
</DataTemplate>
<DataTemplate DataType="{x:Type MainViewModel}">
<!-- here you put your content main screen, prefereably as seperate UserControl -->
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Window>
Then you just set AppBootstrapper.ActiveViewModel = new LoginViewModel() and you have login screen.
If you login, AppBootstrapper.ActiveViewModel = new MainViewModel() and WPF displays main screen.
All that and much more is done by ReactiveUI framwork - only there instead of putting DataTemplates for ViewModels, you register UserControls as views and RoutedViewHost does all the magick. Don't do that on your own, it's inventing the wheel again.
EDIT to answer the comment:
You put AppBootstrapper.ActiveViewModel = new MainViewModel() in your NavigationService. Navigation meaning the thing that changes displayed view. Most common version is a stack, which top is active ViewModel. When you press Back button, you just pop the stack.
This all applies to MVVM model with Model First navigation, which means you first instantiate ViewModel, and navigation service finds the proper view.
You can do this in the other way: View First navigation. There are some tutorials for WPF page navigation. It works exactly the same, but instead of ViewModel, you create a page (a view) which then creates underlying data.
MVVM app model is so popular, because it allows very clean logic and presentation separation (XAML is ONLY about view, ViewModels contain all logic, Models persist the data), which in turn makes it very easy to share logic between platforms. In fact, if you do that correctly, you can use all your ViewModels in apps written in Xamarin, WPF or UWP, just by creating platform-specific views.
To wrap up, WPF allows you to switch in a property data and it will find a view for it automatically (via DataTemplates). Remember about INotifyPropertyChanged and everything will work

WPF TreeView CheckBox Binding - How to populate ViewModel with checked boxes

I'm slightly confused about how to set up a CheckBox with a binding that ensures that my ViewModel is populated with all the checked fields. I have provided some of the code and a description at the bottom.
My Xaml file let's call it TreeView.xaml:
<TreeView x:Name="availableColumnsTreeView"
ItemsSource="{Binding Path=TreeFieldData, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate x:Uid="HierarchicalDataTemplate_1" ItemsSource="{Binding Path=Children, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<CheckBox VerticalAlignment="Center" IsChecked="{Binding IsSelected, Mode=TwoWay}">
<TextBlock x:Uid="TextBlock_1" Text="{Binding DisplayName.Text, Mode=OneWay}" />
</CheckBox>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The "code behind" TreeView.xaml.cs
public partial class MultipleColumnsSelectorView : UserControl
{
public MultipleColumnsSelectorView()
{
InitializeComponent();
}
private MultipleColumnsSelectorVM Model
{
get { return DataContext as MultipleColumnsSelectorVM; }
}
}
The ViewModel (tried to include only the relevant stuff) MultipleColumnsSelectorVM:
public partial class MultipleColumnsSelectorVM : ViewModel, IMultipleColumnsSelectorVM
{
public ReadOnlyCollection<TreeFieldData> TreeFieldData
{
get { return GetValue(Properties.TreeFieldData); }
set { SetValue(Properties.TreeFieldData, value); }
}
public List<TreeFieldData> SelectedFields
{
get { return GetValue(Properties.SelectedFields); }
set { SetValue(Properties.SelectedFields, value); }
}
private void AddFields()
{
//Logic which loops over SelectedFields and when done calls a delegate which passes
//the result to another class. This works, implementation hidden
}
The model TreeFieldData:
public class TreeFieldData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable<TreeFieldData> Children { get; private set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("IsSelected"));
}
}
}
The Problem:
The behaviour that I want is when the user checks a checkbox, it should set the IsSelected property of TreeField (it does that right now) but then I want to go back to the ViewModel and make sure that this specific TreeField is added to SelectedFields. I don't really understand what the PropertyChangedEvent.Invoke does and who will receive that event? How can I make sure that SelectedFields gets populated so when AddFields() is invoked it has all the TreeField data instances which were checked?
You could iterate through the TreeFieldData objects in the TreeFieldData collection and hook up an event handler to their PropertyChanged event and then add/remove the selected/unselected items from the SelectedFields collection, e.g.:
public MultipleColumnsSelectorVM()
{
Initialize();
//do this after you have populated the TreeFieldData collection
foreach (TreeFieldData data in TreeFieldData)
{
data.PropertyChanged += OnPropertyChanged;
}
}
private void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSelected")
{
TreeFieldData data = sender as TreeFieldData;
if (data.IsSelected && !SelectedFields.Contains(data))
SelectedFields.Add(data);
else if (!data.IsSelected && SelectedFields.Contains(data))
SelectedFields.Remove(data);
}
}
The subscriber of the PropertyChanged event is the view, so that if you change IsSelected programmatically the view knows it needs to update.
To insert the selected TreeField into your list you would add this code to your setter.
Also, you could define the following function which makes the notification much easier if you have many properties:
private void NotifyPropertyChange([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
The CallerMemberName attribute instructs the compiler to automatically insert the name of the property calling the method. The ? after PropertyChanged is a shorthand to your comparison to not null.
The setter of IsSelected can then be changed to
set
{
_isSelected = value;
if (value) { viewModel.SelectedFields.Add(this); }
else { viewModel.SelectedFields.Remove(this); }
NotifyPropertyChange();
}
Of course you would need to provide the TreeFieldData with the ViewModel instance, e.g. in the constructor.
I don't know if SelectedFields is bounded/shown in your view. If yes and you want the changes made to the list to be shown, you should change List to ObservableCollection.

WPF Property not updated in UI when modified via derived class

I am unable to display the CurrentStatus property from my ViewModelBase class in the status bar of my WPF application.
ViewModelBase is inherited by TasksViewModel and UserViewModel.
UserViewModel is inherited by ImportViewModel and TestViewModel.
MainWindow has a DataContext of TasksViewModel.
ViewModelBase:
public abstract class ViewModelBase : INotifyPropertyChanged
{
private string _currentStatus;
public string CurrentStatus
{
get { return _currentStatus; }
set
{
if (value == _currentStatus)
{
return;
}
_currentStatus = value;
OnPropertyChanged(nameof(CurrentStatus));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
TasksViewModel:
public class TasksViewModel : ViewModelBase
{
public IEnumerable<ViewModelBase> Collection => _collection;
public override string ViewModelName => "Tasks";
public TasksViewModel()
{
_collection = new ObservableCollection<ViewModelBase>
{
new ImportUsersViewModel(),
new TestFunctionsViewModel()
};
// Added as per John Gardner's answer below.
// watch for currentstatus property changes in the internal view models and use those for our status
foreach (ViewModelBase i in _collection)
{
i.PropertyChanged += InternalCollectionPropertyChanged;
}
}
// Added as per John Gardner's answer.
private void InternalCollectionPropertyChanged(object source, PropertyChangedEventArgs e)
{
var vm = source as ViewModelBase;
if (vm != null && e.PropertyName == nameof(CurrentStatus))
{
CurrentStatus = vm.CurrentStatus;
}
}
}
ImportUsersViewModel:
internal class ImportUsersViewModel : UserViewModel
{
private async void BrowseInputFileAsync()
{
App.Log.Debug("Browsing for input file.");
string path = string.IsNullOrWhiteSpace(InputFile)
? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
: Path.GetDirectoryName(InputFile);
InputFile = FileFunctions.GetFileLocation("Browse for User Import File",
path, FileFunctions.FileFilter.CSVTextAll) ?? InputFile;
CurrentStatus = "Reading Import file.";
ImportUsers = new ObservableCollection<UserP>();
ImportUsers = new ObservableCollection<User>(await Task.Run(() => ReadImportFile()));
string importResult =
$"{ImportUsers.Count} users in file in {new TimeSpan(readImportStopwatch.ElapsedTicks).Humanize()}.";
CurrentStatus = importResult; // Property is updated in ViewModelBase, but not in UI.
}
}
MainWindow.xaml:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="clr-namespace:ViewModel"
xmlns:view="clr-namespace:Views"
x:Class="MainWindow"
Title="Users"
Height="600"
Width="1000"
Icon="Resources/Icon.png">
<Window.Resources>
<DataTemplate DataType="{x:Type viewModel:ImportUsersViewModel}">
<view:ImportUsers />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:TestFunctionsViewModel}">
<view:TestFunctionsView />
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<viewModel:TasksViewModel />
</Window.DataContext>
<DockPanel>
<StatusBar DockPanel.Dock="Bottom" Height="auto">
<TextBlock Text="Status: " />
<!-- Not updated in UI by any View Model -->
<TextBlock Text="{Binding CurrentStatus}" />
</StatusBar>
</DockPanel>
</Window>
If I bind a text block to the CurrentStatus property inside the ImportUsers UserControl it updates without issue, but the "parent" status bar does not update.
My suspicion is that it can't be displayed in the MainWindow status bar because, although both ImportViewModel and TasksViewModel inherit ViewModelBase, they don't have any link to each other, and the TasksViewModel CurrentStatus property isn't being updated.
I think your suspicion is correct.
The DataContext on the Window is a different ViewModel instance to that of the ImportUsersViewModel.
While CurrentStatus is defined in the same object hierarchy, the CurrentStatus line in the ImportUsersViewModel is changing a different object instance than the CurrentStatus property attached to the Window DataContext.
Your window's DataContext is a TaskViewModel, but nothing on that view model is watching for property changes in it's collection, and updating itself. essentially, TasksViewModel is containing the other viewmodels, but not aggregating any of their behaviors.
you need something like:
public class TasksViewModel : ViewModelBase
{
public IEnumerable<ViewModelBase> Collection => _collection;
public override string ViewModelName => "Tasks";
public TasksViewModel()
{
_collection = new ObservableCollection<ViewModelBase>
{
new ImportUsersViewModel(),
new TestFunctionsViewModel()
};
// watch for currentstatus property changes in the internal view models and use those for our status
foreach (var i in _collection)
{
i.PropertyChanged += this.InternalCollectionPropertyChanged;
}
}
}
//
// if a currentstatus property change occurred inside one of the nested
// viewmodelbase objects, set our status to that status
//
private InternalCollectionPropertyChanged(object source, PropertyChangeEvent e)
{
var vm = source as ViewModelBase;
if (vm != null && e.PropertyName = nameof(CurrentStatus))
{
this.CurrentStatus = vm.CurrentStatus;
}
}

how can i get data from view

i use MVVM to built my project, now i have some troubles,when i click a button, i want get data from view to viewmodel, what should i do?
thanks
Bob
Bind that data to the view model and execute a command when the user clicks the button. The command and data are housed in the view model, so it has everything it needs.
public class YourViewModel : ViewModel
{
private readonly ICommand doSomethingCommand;
private string data;
public YourViewModel()
{
this.doSomethingCommand = new DelegateCommand(this.DoSomethingWithData);
}
public ICommand DoSomethingCommand
{
get { return this.doSomethingCommand; }
}
public string Data
{
get { return this.data; }
set
{
if (this.data != value)
{
this.data = value;
this.OnPropertyChanged(() => this.Data);
}
}
}
private void DoSomethingWithData(object state)
{
// do something with data here
}
}
XAML:
<TextBox Text="{Binding Data}"/>
<Button Command="{Binding DoSomethingWithData}"/>
For information on the various dependencies in the above example such as ViewModel and DelegateCommand, see my series of posts on MVVM.
EDIT after receiving more info: For tracking item selection, simply introduce a view model to represent the item:
public class CustomerViewModel : ViewModel
{
private bool isSelected;
public bool IsSelected
{
get { return this.isSelected; }
set
{
if (this.isSelected != value)
{
this.isSelected = value;
this.OnPropertyChanged(() => this.IsSelected);
}
}
}
}
Your "main" view model would expose a collection of these items (generally an ObservableCollection<T>):
public ICollection<CustomerViewModel> Customers
{
get { return this.customers; }
}
Your view would then bind as:
<ListBox ItemsSource="{Binding Customers}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Notice how each ListBoxItem will have its IsSelected property bound to the CustomerViewModel.IsSelected property. Thus, your main view model can just check this property to determine which customers are selected:
var selectedCustomers = this.Customers.Where(x => x.IsSelected);
The solution suggested by Kent is in my opinion by far the best/only one to follow MVVM.
If however you don't want to replicate/reflect listbox selections to the view model or you want a quick and - according to MVVM - dirty solution, you can use the command parameter to send data from the view to the view model.
For that you have to bind the CommandParameter property of the button to the property which contains the data you want to send to the view model. For simplicity I just used a TextBox.
<StackPanel Orientation="Vertical">
<TextBox x:Name="Data"/>
<Button Content="DoSomething"
Command="{Binding Path=DoSomethingCommand}"
CommandParameter="{Binding ElementName=Data, Path=Text}"/>
</StackPanel>
The ViewModel of the sample looks like the following.
public class ViewModel
{
private ICommand doSomethingCommand = new MyCommand();
public ICommand DoSomethingCommand
{
get
{
return doSomethingCommand;
}
}
}
With this, you will get the specified content as the parameter in the Execute method of ICommand.
public class MyCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
string dataFromView = (string)parameter;
// ...
MessageBox.Show(dataFromView);
}
}

Categories