Changing the View for a ViewModel - c#

I am trying to implement the MVVM design patern for mt WPF application. In order to connect the view to the viewmodels, I use a ResourceDictionary (used in Application.Resources), that looks like
<DataTemplate DataType={x:Type viewmodel:SampleViewModel}>
<view:SampleView1 />
</DataTemplate>
The view models are then simply put into content presenters to display them.
Now, when the user presses a button, I'd like to display SampleViewModel using a different view. How do I change the data template used for SampleViewModel?

Less words more code.
As far as you said, you have the class SampleViewModel. I added the property Title for demonstration and ViewType for identifying the correct view:
public enum ItemViewType { View1, View2 };
public class SampleViewModel
{
public string Title { get; set; }
public ItemViewType ViewType { get; set; }
}
The DataTemplateSelector for two views depending on the ViewType property:
class ItemViewTemplateSelector : DataTemplateSelector
{
public DataTemplate View1Template { get; set; }
public DataTemplate View2Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var vm = item as SampleViewModel;
if (vm == null)
return null;
switch (vm.ViewType)
{
case ItemViewType.View1:
return View1Template;
case ItemViewType.View2:
return View2Template;
}
return null;
}
}
Xaml code:
<Window.Resources>
<DataTemplate x:Key="view1Template">
<TextBlock Text="{Binding Title}" Foreground="Red"/>
</DataTemplate>
<DataTemplate x:Key="view2Template">
<TextBox Text="{Binding Title}" />
</DataTemplate>
<local:ItemViewTemplateSelector x:Key="viewTemplateSelector"
View1Template="{StaticResource view1Template}"
View2Template="{StaticResource view2Template}"/>
</Window.Resources>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<StackPanel>
<Button Content="ChangeView" HorizontalAlignment="Center" Command="{Binding SwitchViewCommand}"/>
<ContentControl Content="{Binding ItemViewModel}" ContentTemplateSelector="{StaticResource viewTemplateSelector}"/>
</StackPanel>
The main part is in the class MainViewModel where I've put the logic for switching views:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
this.ItemViewModel = new SampleViewModel { Title = "Some title", ViewType = ItemViewType.View1 };
this.SwitchViewCommand = new RelayCommand(() =>
{
this.ItemViewModel.ViewType = this.ItemViewModel.ViewType == ItemViewType.View1
? ItemViewType.View2
: ItemViewType.View1;
//The magic senquence of actions which forces a contentcontrol to change the content template
var copy = this.ItemViewModel;
this.ItemViewModel = null;
this.ItemViewModel = copy;
});
}
public RelayCommand SwitchViewCommand { get; set; }
private SampleViewModel itemViewModel;
public SampleViewModel ItemViewModel
{
get { return itemViewModel; }
set
{
itemViewModel = value;
RaisePropertyChanged("ItemViewModel");
}
}
}
The SwitchViewCommand can be any type of command, I use the command from the mvvmlight library.
Inside the handler of the command I change the type of viewmodel and update the property ItemViewModel in a tricky way because a ContentControl refreshes a view only if to change the Content property, and this property will not be changed unless you set a reference to different object.
I mean, even the code this.ItemViewModel = this.itemViewModel will not change the view.
It's strange, but the workaround doesn't require much work.

You can achieve this in many different ways depends upon the architecture you want.
You can write a custom DataTemplateSelector and use it on ContentControl.ContentTemplateSelector and choose those two templates appropriately
If this pattern of changing the view occures in many different places and more frequent UX, I would also recommend those two views toggled using a DataTemplate.DataTrigger based on a property in SampleViewModel [I am guessing you might have a distinguishing property in the ViewModel to know that state]

You can override the mapping by placing a similar resource lower down in the tree. Since WPF will resolve the resource by searching upwards, such an override will replace your existing mapping.

Related

Selecting the last TabItem when new items are added to a TabControl's ItemSource using MVVM

I have created a dynamically generated TabControl by binding ItemsSource to MyUnicornsViewModel.
As new items are added to MyUnicornsViewModel... new tab items are created. However, the newly added tabs are not automatically selected in the TabControl.
How can I get new tabs to be selected when they are added?
<TabControl ItemsSource="{Binding MyUnicornsViewModel}" SelectedItem="{Binding SelectedItem}">
<TabControl.ItemTemplate>
<!-- header template -->
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- body template-->
<DataTemplate>
<TextBlock Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
At first, I was hoping there was an event for "ItemsChanged" or "ItemAdded" in the TabControl, that way I can set the SelectedIndex in the code-behind as new items are added.
Another thing I tried was to bind the TabControl.SelectedItem to a SelectedItem property in MyUnicornsViewModel. Sadly, that didn't work either.
MyUnicornsViewModel:
public class MyUnicornsViewModel : ObservableCollection<UnicornViewModel>
{
...
private void AddNewUnicorn()
{
var awesomeUnicorn = new UnicornViewModel();
Add(awesomeUnicorn);
SelectedItem = awesomeUnicorn; //I expected my TabControl to have 'awesomeUnicorn' selected.
}
public UnicornViewModel SelectedItem { get; set; }
}
There are a couple of issues here:
It's very odd to derive a "view model" from ObservableCollection. A view model should contain an observable collection.
View models need to implement the INotifyPropertyChanged interface; it's not clear from the code provide if UnicornViewModel implements this interface, however, MyUnicornsViewModel absolutely does not.
Here's some suggestions:
A view model base class that implements the INotifyPropertyChanged interface will really help get you most of the way. You can write your own using the INotifyPropertyChanged documentation or look for an MVVM framework that fits well with your project (e.g. Prism, MVVM Light, ReactiveUI). Each of these will provide a base class to use for view models - BindableBase, ViewModelBase, ReactiveObject respectively for each of the frameworks above.
MyUnicornsViewModel should have:
An ObservableCollection for the collection of unicorns; this will be bound to the ItemsSource property on your TabControl.
The SelectedItem property must fire the PropertyChanged event when set.
Here's a quick sample using Prism:
public sealed class UnicornViewModel : BindableBase
{
public UnicornViewModel(string name, string content)
{
Name = name;
Content = content;
}
// these properties don't change and therefore don't need to raise property changed
public string Name { get; }
public string Content { get; }
}
public sealed class UnicornsViewModel : BindableBase
{
private UnicornViewModel _selectedUnicorn;
public UnicornsViewModel()
{
AddUnicornCommand = new DelegateCommand(AddUnicorn);
ClearUnicornsCommand = new DelegateCommand(ClearUnicorns, () => HasUnicorns).ObservesProperty(() => HasUnicorns);
}
public ObservableCollection<UnicornViewModel> Unicorns { get; } = new ObservableCollection<UnicornViewModel>();
public UnicornViewModel SelectedUnicorn
{
get => _selectedUnicorn;
set => SetProperty(ref _selectedUnicorn, value, () => RaisePropertyChanged(nameof(HasUnicorns)));
}
public DelegateCommand AddUnicornCommand { get; }
public DelegateCommand ClearUnicornsCommand { get; }
private bool HasUnicorns => Unicorns.Any(); // helper property for the clear command's can execute
private void AddUnicorn()
{
Unicorns.Add(new UnicornViewModel($"Unicorn {Unicorns.Count + 1}", Guid.NewGuid().ToString()));
SelectedUnicorn = Unicorns.Last();
}
private void ClearUnicorns()
{
SelectedUnicorn = null;
Unicorns.Clear();
}
}

How do I bind the data of a custom UserControl

So I just setup a project and added a custom UserControl that looks like this.
<Grid>
<ItemsControl ItemsSource="{Binding UserViewModel.Users}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<controls:UserCard/>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
As you can see I tried binding the Text property buti it doesn't bind.
Now there could be a lot of reasons to why it's behaving like this so I will try to narrow it down.
I've created a BaseViewModel that will hold my ViewModels and it looks like this.
public class BaseViewModel : ObservableObject
{
public UserViewModel UserViewModel { get; set; } = new UserViewModel();
}
And then I've setup my ViewModel like this
public class UserViewModel : ObservableObject
{
public ObservableCollection<User> Users { get; set; } = new ObservableCollection<User>();
public UserViewModel()
{
Users.Add(new User{Name = "Riley"});
Users.Add(new User{Name = "Riley1"});
}
}
Simple, now I do have a ObservableObject that looks like this and deals with the INPC
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And in my MainView.xaml
I've set the DataContext like so
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new BaseViewModel();
}
}
It's the exact same for the UserControl
And this is where I actually add the UserControl so it displays in the MainWindow
<ItemsControl ItemsSource="{Binding UserViewModel.Users}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<controls:UserCard/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Now the issue is that it doesn't bind the Data, I want to display the Name property from the Model but it's not displaying it and I am not sure why, if I try to bind it to a TextBlock property in the MainView directly it works fine.
I am unsure to why it's behaving like this and I would like to understand why.
Do I need to make use of DependencyProperties? Or is it just a case of me creating a new instance of the BaseViewModel? Where did I go wrong?
Your MainViewWindow contains an ItemsControl with the binding ItemsSource="{Binding UserViewModel.Users}", with each item being displayed with a <controls:UserCard/>. But your user control is then trying to bind to the list again with "{Binding UserViewModel.Users}". Why are you trying to display a list inside another list?
I suspect the problem here is that you think your custom UserControl's DataContext is still pointing to the BaseViewModel, like its parent. It isn't. The DataContext of each item in an ItemsControl points to it's own associated element in the list, i.e. an instance of type User.
UPDATED: Let's say you have a main view model with a list of child view models, like this:
public class MainViewModel : ViewModelBase
{
public MyChildViewModel[] MyItems { get; } =
{
new MyChildViewModel{MyCustomText = "Tom" },
new MyChildViewModel{MyCustomText = "Dick" },
new MyChildViewModel{MyCustomText = "Harry" }
};
}
public class MyChildViewModel
{
public string MyCustomText { get; set; }
}
And let's say you set your MainWindow's DataContext to an instance of MainViewModel and add a ListView:
<ListView ItemsSource="{Binding MyItems}" />
If you do this you'll see the following:
What's happening here is that the ListView is creating a container (of type ContentPresenter) for each of the three elements in the list, and setting each one's DataContext to point to its own instance of MyChildViewModel. By default ContentPresenter just calls 'ToString()' on its DataContext, so you're just seeing the name of the class it's pointing to. If you add a ToString() operator to your MyChildViewModel like this:
public override string ToString()
{
return $"MyChildViewModel: {this.MyCustomText}";
}
... then you'll see that displayed instead:
You can also override the ListViewItem's template entirely, and since it already points to its associated instance of MyChildViewModel you can just bind directly to its properties:
<ListView ItemsSource="{Binding MyItems}">
<ListView.ItemTemplate>
<DataTemplate>
<!-- One of these gets created for each element in the list -->
<Border BorderBrush="Black" BorderThickness="1" Background="CornflowerBlue" CornerRadius="5" Padding="5">
<TextBlock Text="{Binding MyCustomText}" Foreground="Yellow" />
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Which will change the display to this:
Make sense?

MVVM is command in model a bad practice?

I'm wondering, is putting command into model is bad practice in MVVM. For example, I have ListBox with Image and Button. When I click on Button I need to open url in browser. So, my code will look like this:
<ListBox ItemSource="{Binding Items">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source={Binding ImageSource} />
<Button Content="Open url" Command={Binding OpenUrlCommand}"/>
</StackPanel>
</DataTemplate>
</Listbox.ItemTemplate>
</ListBox>
ViewModel:
class MainViewModel:BaseViewModel
{
public ObservableCollection<Model> Items {get;set;}
}
Model:
class Model
{
public string ImageSource {get;set;}
public string Url {get;set;}
public ICommand OpenUrlCommand {get;set;}
public Model()
{
OpenUrlCommand = new RelayCommand(openUrl);
}
public void openUrl()
{
Process.Start(Url); //Open url in browser
}
}
Is it ok, or I should move OpenUrlCommand to MainViewModel ?
You actually should implement the method in the Model, but the command in the ViewModel. This would be much more aligned to the MVVM architecture, and would be no additional work at all for you in this case.
In your ViewModel:
class MainViewModel : BaseViewModel
{
private Model _model;
ICommand OpenUrlCommand { get { return new RelayCommand(_model.openUrl); } }
// ...
}
If you want to "send" some URL from the View, you can use CommandParameter for that, and have a typed RelayCommand.
UPDATE:
Note that, since the DataTemplate where the Command binding resides is the ItemTemplate, you should implement the Command in some ItemViewModel, not in the MainViewModel. Then, make Items an ObservableCollection<ItemViewModel> instead of ObservableCollection<Model>, and initialize the collection like this:
IEnumerable<Model> models= getSomeModelsToStartWith();
var Items = new ObservableCollection<ItemViewModel>(models.Select(m => new ItemViewModel(m));

WPF Mvvm navigation with parameters

Following this tutorial (among others) and reading questions asked here I've constructed a navigation mechanism that will allow me to pass parameters between my ViewModels:
Object base - every view model inherits from it:
public abstract class ObjectBase : INotifyPropertyChanged
{
//INotifyPropertyChanged members
...
//Navigation handling
public abstract ObjectBase BackLocation { get; }
public abstract event Action<ObjectBase> NavigateTo;
public abstract string ViewHeader { get; }
}
MainViewModel - in charge of navigation:
public class MainViewModel : ObjectBase
{
private ObjectBase _selectedView;
private CommandBase _backCommand;
public MainViewModel()
{
SelectedView = new FirstViewModel();
}
public ObjectBase SelectedView
{
get { return _selectedView; }
set
{
SetProperty(ref _selectedView, value);
//register to the navigation event of the new view
SelectedView.NavigateTo += (target)=> { SelectedView = target; };
}
}
//This command is bound to a Back button on the main view
public CommandBase BackCommand
{
get { return _backCommand ?? (_backCommand = new CommandBase(Back)); }
}
private void Back(object obj)
{
if (SelectedView.BackLocation != null)
{
SelectedView = SelectedView.BackLocation;
}
else
{
Application.Current.Shutdown();
}
}
}
And the main view:
<Window ...
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type vm:FirstViewModel}">
<views:FirstView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding SelectedView}"/>
</Window>
My problem is: If I set the DataTemplates in the main view like the above it makes each view aware of it's DataContext so if I want to add the DataContext explicitly to a view in order to use intellisense like this:
<UserControl x:Class="Wpf_NavigationTest.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Wpf_NavigationTest.ViewModels">
<!--this causes the view model's constructor to get called again-->
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
<Grid>
<TextBlock Text="User control 1" FontSize="40"/>
</Grid>
the View Model's constructor is called twice, losing the parameters passed by the Navigate event.
The problem here is that you are setting the DataContext inside your UserControl, and also in your main view model.
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
The code above is instantiating a new FirstViewModel every time this UserControl is created. Therefore when the control gets created by the ContentControl (based on the DataTemplate), it then goes ahead and also creates a new FirstViewModel.
So, the solution here is to remove the UserControl.DataContext declaration in the UserControl, and you can instead set the DataContext of the ContentControl to that of your SelectedView.
<ContentPresenter Content="{Binding SelectedView}"
DataContext="{Binding SelectedView}"/>
In order to use multiple view models to a single view, you can simply add another DataTemplate:
<DataTemplate DataType="{x:Type vm:ThirdViewModel}">
<views:SecondView/>
</DataTemplate>
For Design-Time data (to get the intellisense), you can make use of d:DataContext as explained in this article.
This will require you to set up some view models as static resources, I would recommend creating them in a separate ResourceDictionary.

ContentControl not updating

I'm trying to have a MainWindow that is bound to the a view. I change that view in code and expect it to update in the Main Window, however that is not happening.
I have this code in my XAML
<Grid>
<ContentControl Content="{Binding Source={StaticResource ViewModelLocator}, Path=MainWindowViewModel.CurrentControl}" />
</Grid>
I then change my Control via this code
public class MainWindowViewModel : ReactiveObject
{
private UserControl _CurrentControl = null;
public UserControl CurrentControl
{
get
{
if (_CurrentControl == null)
{
_CurrentControl = new HomePage();
}
return _CurrentControl;
}
set
{
this.RaiseAndSetIfChanged(x => x.CurrentControl, value);
}
}
}
As you can see I'm using the ReactiveUI library.
Is ContentControl the wrong thing to use in that view or am I just not binding and updating correctly?
There is actually a far better way to do this, using ViewModelViewHost:
<Grid DataContext="{Binding ViewModel, ElementName=TheUserControl}">
<ViewModelViewHost ViewModel="{Binding CurrentControlViewModel}" />
</Grid>
Now, your class will look something like:
public class MainWindowViewModel : ReactiveObject
{
private ReactiveObject _CurrentControlViewModel = new HomePageViewModel();
public ReactiveObject CurrentControlViewModel {
get { return _CurrentControl; }
set { this.RaiseAndSetIfChanged(x => x.CurrentControlViewModel, value); }
}
}
And somewhere in your app's startup, you should write:
RxApp.Register(typeof(IViewFor<HomePageViewModel>), typeof(HomePage));
What's ViewModelViewHost?
ViewModelViewHost will take a ViewModel object that you provide via Bindings, and look up a View that fits it, using Service Location. The Register call is how you can associate Views with ViewModels.
why you call your class MainWindowViewModel? when you wanna do mvvm you shouldn't have properties with type UserControl in your VM.
the usual mvvm way looks like this:
viewmodel with INotifyPropertyChanged
public class MyViewmodel
{
public IWorkspace MyContent {get;set;}
}
xaml content control with binding to your VM
<ContentControl Content="{Binding MyContent}"/>
datatemplate --> so that wpf knows how to render your IWorkspace
<DataTemplate DataType="{x:Type local:MyIWorkSpaceImplementationType}" >
<view:MyWorkspaceView />
</DataTemplate>
I think you have several muddled concepts here and they are getting in each others way.
Firstly you aren't actually using ANY of the reactiveUI code, it never gets called. Since your get accessor implements a lazy instantiation pattern then it means the set accessor is ignored. This means that the view never notifies the property change, so you never get updates.
I'd recommend using something more like
private UserControl _currentControl;
public MainWindowVirwModel()
{
CurrentControl = new HomePage();
}
public UserControl CurrentControl
{
get { return _curentControl;}
set { this.RaiseAndSetIfChanged(...); }
}
In addition, this still mixes up View components i.e. HomePage, inside your ViewModel tier which will making unit testing far more difficult.

Categories

Resources