I've a question on VM communication.
Here's my code in C#/WPF app.On my MainWindow.xam,I've a button.
On click of this button,
I need to access and modify the ProductList collection from within another ViewModel.
How do I achieve this please?
public List<ProductInfo> ProductList { get; private set; }
private MainWindow m_mvWindow = null;
public MainWindowViewModel(MainWindow window)
{
this.m_mvWindow = window;
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel(this);
}
}
Thanks.
The simplest solution would be to expose your other VM as a property of MainWindowViewModel, pass the child VM a reference to the ProductList collection and have an ICommand on the child VM which is bound to the button in your XAML and which handles the collection modifications.
Something like this:
Main VM
public class MainViewModel
{
<!-- Your stuff ->
public ChildViewModel ChildViewModel
{
if(_childViewModel == null)
{
_childViewModel = new ChildViewModel(ProductList)
}
return _childViewModel;
}
}
Child VM
public class ChildViewModel
{
private List<ProductInfo> _products;
public DelegateCommand ClearCollection {get; set;}
public ChildViewModel(List<ProductInfo> products)
{
_products = products;
ClearCollection = new DelegateCommand(OnClearCollection);
}
private void OnClearCollection()
{
_products.Clear();
}
}
And in the xaml...
<Button Command={Binding ChildViewModel.ClearCommand} Content="..." />
You can use either of these ways:
Create an event in the other view model and handle it in the main view model. That would be the preferred way, since it does avoid coupling your child VM to the main VM, which is poor design.
Pass the collection to the child VM in a constructor. Try to avoid passing the full main VM, again to avoid coupling.
Related
I've been trying to do the following thing in WPF:
A window with a login-page and a home-page.
Upon successfully authenticating a user, the window should display the home-page.
It should work by using the native WPF dependency injection framework.
But also...
There might be a page 3, 4, 5 and each of these pages should be able to call one another.
And maybe each of these pages could have pages inside them that can also call each other.
So the solution should be able to work with nested pages and navigations if possible.
What I have:
So, after looking for solutions in the stack forum I ended up with this composition approach.
Starting by the App.xaml, all services and viewmodels are initialized and the main window receives its viewmodel by injection:
private void ConfigureServices(ServiceCollection services)
{
services.AddSingleton<MainWindow>();
//ViewModels
services.AddSingleton<MainViewModel>();
services.AddSingleton<AuthViewModel>();
services.AddSingleton<HomeViewModel>();
}
private void OnStartup(object sender, StartupEventArgs e)
{
var mainWindow = serviceProvider.GetService<MainWindow>();
mainWindow.DataContext = serviceProvider.GetService<MainViewModel>();
mainWindow.Show();
}
Then, the mainViewModel receives by injection every other viewmodel and stores them in a property.
public class MainViewModel
{
public IPageViewModel SelectedPage {get; set; } //PropertyChanged() removed for brevity.
public ObservableCollection<IPageViewModel> Pages {get; set;}
public MainViewModel(AuthViewModel authViewModel, HomeViewModel homeViewModel)
{
this.Pages = new ObservableCollection<IPageViewModel>() { authViewModel, homeViewModel};
this.SelectedPage = this.Pages.First();
}
}
All page viewmodels inherit from this interface so they can be retrieved from the collection by name and then added as the SelectedPage when needed.
public interface IPageViewModel : INotifyPropertyChanged
{
public string PageTitle { get; set; }
}
The window has a content control with a property content bound to the SelectedPage so it's updated.
<Window>
<ContentControl Content="{Binding SelectedPage}" />
</Window>
And it knows which view to use for each viewmodel by these data templates.
<Application.Resources>
<DataTemplate DataType="{x:Type vm:AuthViewModel}">
<views:AuthView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:HomeViewModel}">
<views:HomeView />
</DataTemplate>
</Application.Resources>
But then... I noticed that this won't work, I can only call changes on the SelectedPage from within the mainViewModel.
public class AuthViewModel : BaseViewModel
{
public AuthViewModel() { }
public void AttemptLogin() {
// how
SelectedPage = Pages[1];
}
}
Issues
I could perhaps inject the mainviewmodel in all child models, but that would not look good and in fact from the start a lot of things are kind of a mess.
For example, I have to:
Add a service viewmodel for every viewmodel I create to the app.xaml.
Add each one of them as a parameter of the mainwindow viewmodel which looks ugly.
I'm probably doing this very wrong, I need help.
There are many possible solutions. A simple one is to introduce an event.
I also recommend to move and restrict the responsibility to select view models to the MainViewModel. Other page models should not be aware of the flow like who selects who. Otherwise this would add a too tight coupling, which is avoidable at this point.
public class MainViewModel
{
public IPageViewModel SelectedPage { get; set; }
private Dictionary<string, IPageViewModel> Pages { get; }
public MainViewModel(AuthViewModel authViewModel, HomeViewModel homeViewModel)
{
authViewModel.AuthenticationPassed += OnAuthenticationSuccessfull;
this.Pages = new Dictionary<string, IPageViewModel>()
{
{ nameof(AuthViewModel), authViewModel },
{ nameof(HomeViewModel), homeViewModel }
};
this.SelectedPage = this.Pages[nameof(AuthViewModel)];
}
public OnAuthenticationSuccessfull(object sender, EventArgs e)
{
(sender as AuthViewModel).AuthenticationPassed -= OnAuthenticationSuccessfull;
this.SelectedPage = this.Pages[nameof(HomeViewModel)];
}
}
class AuthViewModel
{
public event EventHandler AuthenticationPassed { get; }
...
}
I am using a Gong Framework in my project.
I've created the following DropHandler.
Xaml:
<ListBox ItemsSource="{Binding Collection}" dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}"/>
ViewModel:
class MyViewModel : IDropTarget
{
ObservableCollection<SomeType> Collection;
public void DragOver(IDropInfo dropInfo)
{
// ...
}
public void Drop(IDropInfo dropInfo)
{
// ...
}
}
Problem. I want to and another DropHandler for another ListBox in this window. But I don't know, how can I do it. How can I implement interface IDropTarget again?
You can't implement the interface "again" in the same class but you could bind the DropHandler property to an IDropTarget property of the view model:
dd:DragDrop.DropHandler="{Binding FirstDropTarget}"
You would then create a new class to handle the dropping. If you need a reference to the view model, you could inject it into the IDropTarget implementation, e.g.:
class MyViewModel
{
ObservableCollection<SomeType> Collection;
public MyViewModel()
{
FirstDropTarget = new YourHandler(this);
SecondDropTarget = new YourOtherHandler(this);
}
public IDropTarget FirstDropTarget { get; }
public IDropTarget SecondDropTarget { get; }
}
I have several different ViewModels that I would like to display in the same view (MainPage.xaml).
I'm new to this and don't know how to do it. I have tried to create a MainViewModel:
public class MainViewModel : ViewModelBase, INotifyPropertyChanged
{
WeatherViewModel weatherView = new WeatherViewModel();
ForecastViewModel forecastViewModel = new ForecastViewModel();
DeparturesViewModel departuresViewModel = new DeparturesViewModel();
CalenderViewModel calenderViewModel = new CalenderViewModel();
}
public void GetAllViews()
{
weatherView.GetCurrentTemp();
forecastViewModel.GetForecastTemp();
departuresViewModel.GetDepartures();
calenderViewModel.GetCalender();
}
And in my MainPage.xaml.cs I have this:
public MainPage()
{
this.InitializeComponent();
this.DataContext = new MainViewModel();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as MainViewModel;
vm.GetAllViews();
}
I manage to display each ViewModel individually like this instead:
this.DataContext = new WeatherViewModel();
but I would like to display everything in the same View.
I think you're on the right track but missed some small but important pieces.
In your example code the MainViewModel class is currently setup with private fields where you really need public properties. Additionally, I would make sure ViewModelBase implements INotifyPropertyChanged if it's not already; that way none of the classes deriving from ViewModelBase need to worry about that part.
public abstract class ViewModelBase : INotifyPropertyChanged
{
/* INotifyPropertyChanged implementation +
whatever other common behavior makes sense
belongs in this class
*/
}
public class MainViewModel : ViewModelBase
{
public WeatherViewModel Weather { get; } = new WeatherViewModel();
public ForecastViewModel Forecast { get; } = new ForecastViewModel();
public DeparturesViewModel Departures { get; } = new DeparturesViewModel();
public CalendarViewModel Calendar { get; } = new CalendarViewModel();
}
In your view code behind file you're setting the data context to 2 different instances of MainViewModel - once in the constructor and once in the Loaded event handler. I'd stick with the constructor version or instead you could set the data context in XAML like this:
<MainPage.DataContext>
<MainViewModel>
</MainPage.DataContext>
Once the data context for the main page is setup and the view models are public properties then you can use bindings to access the state (properties) of the view models perhaps something like this:
<TextBlock Text='{Binding Path=Weather.CurrentTempCelsius, StringFormat='Current Temp: {0}°C'}' />
Multiple ViewModels in same View
You have many ways to approach. Fist way using x:bind. You could initialize each view model in the page resource and give them x:Name, then using x:bind to access specific property like following.
<Page.Resources>
<local:CalenderViewModel x:Name="CalenderViewModel"/>
<local:DeparturesViewModel x:Name="DeparturesViewModel"/>
<local:ForecastViewModel x:Name="ForecastViewModel"/>
<local:WeatherViewModel x:Name="WeatherViewModel"/>
</Page.Resources>
<Grid>
<TextBlock Text="{x:Bind WeatherViewModel.temperature}"/>
</Grid>
Other Way is that integrate all the ViewModels into MainViewModel. And coding.monkey provide the correct solution that you could refer directly.
I'm struggling to find a solution to the problem of having to maintain two lists.
I'm using MVVM, but don't want my model to use ObservableCollection. I feel this is best to encapsulate and allows me to use different views/patterns (a console for example). Instead of setting up my structure like this:
public class MainWindow {
// handled in XAML file, no code in the .cs file
}
public abstract class ViewModelBase : INotifyPropertyChanged {
// handles typical functions of a viewmodel base class
}
public class MainWindowViewModel : ViewModelBaseClass {
public ObservableCollection<Account> accounts { get; private set; }
}
public class Administrator {
public List<Account> accounts { get; set; }
public void AddAccount(string username, string password) {
// blah blah
}
}
I would like to avoid having two different collections/lists in the case above. I want only the model to handle the data, and the ViewModel to responsible for the logic of how its rendered.
what you could do is to use a ICollectionView in your Viewmodel to show your Model Data.
public class MainWindowViewModel : ViewModelBaseClass {
public ICollectionView accounts { get; private set; }
private Administrator _admin;
//ctor
public MainWindowViewModel()
{
_admin = new Administrator();
this.accounts = CollectionViewSource.GetDefaultView(this._admin.accounts);
}
//subscribe to your model changes and call Refresh
this.accounts.Refresh();
xaml
<ListBox ItemsSource="{Binding accounts}" />
I am really struggling to understand binding. I know there are loads of other threads with much the same title as this one, but they're all trying to do something more complex than I am, and all the answers assume a whole pile of stuff that I just don't get :(
I'm trying to display a dynamically updated message log. I've defined a Message class:
public class Message
{
public DateTime Timestamp { get; private set; }
public string Value { get; private set; }
public int Severity { get; private set; }
public Message(string value, int severity)
{
Timestamp = DateTime.Now;
Value = value;
Severity = severity;
}
}
I've defined a MessageLog class as simply:
public class MessageLog: ObservableCollection<Message>
{
public MessageLog(): base()
{ }
}
In my MainWindow constructor I have a Log property:
public MessageLog Log { get; private set; }
In the MainWindow constructor I initialise Log:
public MainWindow()
{
InitializeComponent();
DataContext = this;
Log = new Model.MessageLog();
// and so on
}
In the XAML for the main window I have:
<ListBox Name="MessagePanel" Height="100" ItemsSource="{Binding MessageLog}" IsEnabled="False"/>
Now if I add Message instances to the MessageLog I expected to see them appear in the ListBox. They don't. What have I missed?
Thanks in advance (and if you can point me somewhere that explains bindings clearly -- especially the view that XAML has of the code and where it can look for things -- then many more thanks on top. At the moment I'm using Matthew McDonald's "Pro WPF 4.5 in C#" and I'm just not getting it.)
Change your constructor:
public MainWindow()
{
InitializeComponent();
DataContext = this;
Log = new Model.MessageLog();
}
to this:
public MainWindow()
{
InitializeComponent();
Log = new Model.MessageLog(); // <- This line before setting the DataContext
DataContext = this;
}
Explanation:
Setting properties after having set the DataContext requires your class to implement INotifyPropertyChanged and raise change notifications after properties are set.
Since you're setting the DataContext before setting the property, the value of this.Log is null at the time of DataBinding, and WPF is never notified that it ever changed.
That being said, you don't usually put Data inside UI Elements (such as Window). The accepted and recommended approach to WPF is MVVM, where you usually create a ViewModel and set that as the Window's DataContext:
public class MyViewModel
{
public MessageLog Log {get;set;}
public MyViewModel()
{
Log = new MessageLog();
}
}
Window Constructor:
public MainWindow
{
DataContext = new MyViewModel();
}
Your collection property name is Log which is what you should be binding to in ItemsSource property; and if you have not done a typo in your question then you are binding wrongly to MessageLog, and change Binding as below:
<ListBox Name="MessagePanel" Height="100" ItemsSource="{Binding Log}" IsEnabled="False"/>
For more information and learning on Data Binding in WPF (4.5), see MSDN Data Binding Overview
The datacontext of the view must be the viewmodel.