Catel: multiple ViewModels with one View. Is it possible? - c#

I got a generic base-class for ViewModel of my user-control:
public class SuggestModule<TEntity> : ViewModelBase
where TEntity : class, ISuggestable, new()
{
public SuggestModule(ISomeService someService)
{
// Some logic
}
// Some private fields, public properties, commands, etc...
}
}
Whitch has many inheritable classes. That is two of them, for example:
public class CitizenshipSuggestViewModel : SuggestModule<Citizenship>
{
public CitizenshipSuggestViewModel(ISomeService someService)
: base(someService) { }
}
public class PlaceOfBirthSuggestViewModel : SuggestModule<PlaceOfBirth>
{
public PlaceOfBirthSuggestViewModel(ISomeService someService)
: base(someService) { }
}
That is view implementation:
<catel:UserControl
x:Class="WPF.PRC.PBF.Views.UserControls.SuggestUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://schemas.catelproject.com"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:pbf="clr-namespace:WPF.PRC.PBF">
<Grid>
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"> />
<ListBox ItemsSource="{Binding ItemsCollection}" />
// Other elements, behaviors, other extensive logic...
</Grid>
</catel:UserControl>
Now, in MainWindow creating two ContentControls:
<catel:Window
x:Class="WPF.PRC.PBF.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://schemas.catelproject.com">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" Content="{Binding CitizenshipSuggestViewModel, Converter={catel:ViewModelToViewConverter}}" />
<ContentControl Grid.Row="1" Content="{Binding PlaceOfBirthSuggestViewModel, Converter={catel:ViewModelToViewConverter}}" />
</Grid>
</catel:Window>
Due to violation of the Naming Convention, manually resolving a ViewModel in App.xaml.cs:
var viewModelLocator = ServiceLocator.Default.ResolveType<IViewModelLocator>();
viewModelLocator.Register(typeof(SuggestUserControl), typeof(CitizenshipSuggestViewModel));
viewModelLocator.Register(typeof(SuggestUserControl), typeof(PlaceOfBirthSuggestViewModel));
var viewLocator = ServiceLocator.Default.ResolveType<IViewLocator>();
viewLocator.Register(typeof(CitizenshipSuggestViewModel), typeof(SuggestUserControl));
viewLocator.Register(typeof(PlaceOfBirthSuggestViewModel), typeof(SuggestUserControl));
But now I have two views with identical ViewModels.
How can I solve this problem without creation of identical Views with repetition of the code in each of them?
Thank you in advance!

You have a few options:
Repeat yourself so if one needs customization in the future, you can customize just 1 without much overhead. The downside is that if you need to check the generic behavior, you'll need to change them all.
Create an enum representing the state that a VM is representing. In that case, you can simply creat a single vm that catches all of the cases that you need to handle. You can solve this using a dependency property on the view and by using ViewToViewModelMapping to automatically map this to the vm. This comes closest to achieving code-reusage as you wish to achieve with your view. It does go a bit against "separation of concerns", but since it represents the same sort of data I believe it's still a good approach.
For 2, you need to do the following:
1 Create an enum SuggestEntityType with PlaceOfBirth, Citizenship, etc
2 Create a property on the vm (this example code is assuming you are using Catel.Fody):
public SuggestedEntityType EntityType { get; set; }
3 Create a dependency property on the view:
[ViewToViewModel(MappingType = ViewToViewModelMappingType.ViewToViewModel)]
public SuggestedEntityType EntityType
{
get { return (SuggestedEntityType) GetValue(EntityTypeProperty); }
set { SetValue(EntityTypeProperty, value); }
}
public static readonly DependencyProperty EntityTypeProperty = DependencyProperty.Register("EntityType", typeof (SuggestedEntityType),
typeof (MyControl), new PropertyMetadata(null));
4 You can now use the user control like this:
<controls:MyView EntityType="Citizenship" />
For more info, see http://docs.catelproject.com/vnext/catel-mvvm/view-models/mapping-properties-from-view-to-view-model/

One possibility is to create a dependencyProperty in your UserControl codebehind, for example
#region Properties
public string Test
{
get { return (string)GetValue(TestProperty); }
set { SetValue(TestProperty, value); }
}
#endregion Properties
#region Dependency Properties
public static readonly System.Windows.DependencyProperty TestProperty =
System.Windows.DependencyProperty.Register("Test", typeof(string), typeof(YourUserControl), new System.Windows.FrameworkPropertyMetadata() { BindsTwoWayByDefault = true });
#endregion Dependency Properties
Then in your xaml you can bind this property as :
<TextBlock Text="{Binding Test, RelativeSource={RelativeSource AncestorType={x:Type catel:UserControl}, Mode=FindAncestor}}">
Then in your MainWindow you can write:
<views:YourUserControlName Test="{Binding SomeTextPropertyFromMainWindowVM}"/>
So you will be able to bind from the property SomeTextPropertyFromMainWindowVM in your windowVM to some property in your userControl.
If you have in your main window several viewModels, you can write like:
<views:YourUserControlName Test="{Binding SomeViewModel.SomeTextProperty}"/>
<views:YourUserControlName Test="{Binding SomeOtherViewModel.SomeTextProperty}"/>

Related

Is there a way to pass an object parameter as a reference to a WPF Usercontrol

I am struggling to get a user control to accept a property from my Data Context object. I don't want to pass just the value; but the instance of the property because I would like to have converters operate on the attributes of the property.
I am very new to the WPF space, I've read many articles and none of them don't address this issue. The reason I'm trying to do this is because I have a calculations class that has many properties that need to be displayed and I don't really want to create a user control for each property or have 2,000 lines of repetitious XAML.
Any insight would be greatly appreciated.
Example Class
public class MyClass
{
[MyAttribute("someValue")]
public string Foo { get; set; }
}
View Model
public class MyViewModel : INotifyPropertyChanged
{
private _myClass;
public MyClass MyClass1
{
get => _myClass;
set
{
if(_myClass != value)
{
_myClass = value;
OnPropertyChanged();
}
}
}
}
Parent XAML
<UserControl DataContext="MyViewModel">
<Grid>
<!-- this is where I'm struggling, I think -->
<uc:MyConsumerControl ObjectProp="{Binding Path=MyClass1.Foo}"/>
</Grid>
</UserControl>
User Control
XAML
<UserControl DataContext={Binding RelativeSource={RelativeSource Mode=Self}}>
<Grid>
<TextBox Text="{Binding ObjectProp}"/>
<TextBlock Text="{Binding Path=ObjectProp, Converter={StaticResource MyAttrConverter}}"/>
</Grid>
</UserControl>
C#
public class MyConsumer : UserControl
{
public MyConsumer { InitializeComponent(); }
public object ObjectProp
{
get => (object)GetValue(ObjDepProp);
set => SetValue(ObjDepProp, value);
}
public static readonly DependencyProperty ObjDepProp =
DependencyProperty.Register(nameof(ObjectProp),
typeof(object), typeof(MyConsumer));
}
First of all, there is a naming convention for identifier fields of dependency properties:
public static readonly DependencyProperty ObjectPropProperty =
DependencyProperty.Register(nameof(ObjectProp), typeof(object), typeof(MyConsumer));
public object ObjectProp
{
get => GetValue(ObjectPropProperty);
set => SetValue(ObjectPropProperty, value);
}
Second, a UserControl that exposes bindable properties must never set its own DataContext, so this is wrong:
<UserControl DataContext={Binding RelativeSource={RelativeSource Mode=Self}}>
The XAML should look like this:
<UserControl ...>
<Grid>
<TextBox Text="{Binding ObjectProp,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
<TextBlock Text="{Binding Path=ObjectProp,
RelativeSource={RelativeSource AncestorType=UserControl}, />
Converter={StaticResource MyAttrConverter}}"
</Grid>
</UserControl>
Finally, this is also wrong, because it only assigns a string to the DataContext:
<UserControl DataContext="MyViewModel">
It could probably look like shown below - although that would again explicitly set the DataContext of a UserControl, but perhaps one that could be considered a top-level view element like a Window or Page.
<UserControl ...>
<UserControl.DataContext>
<local:MyViewModel/>
</UserControl.DataContext>
<Grid>
<uc:MyConsumerControl ObjectProp={Binding Path=MyClass1.Foo}
</Grid>
</UserControl>

Two Views in one ViewModel (WPF/MVVM)

PROBLEM: Use one single viewModel with two different views.
I have a Window with a control ContentControl which is binded to a property in the DataContext, called Object MainContent {get;set;}. Base on a navigationType enum property, I assign other ViewModels to it to show the correct UserControl.
I need to merge two views into one ViewModel, and because I'm assigning a ViewModel to the ContentControl mentioned before, the TemplateSelector is not able to identify which is the correct view as both shares the same viewModel
If I assign the view instead the ViewModel to the ContentControl, the correct view is shown, however, non of the commands works.
Any Help? Thanks in advance.
SOLUTION: based on #mm8 answer and https://stackoverflow.com/a/5310213/2315752:
ManagePatientViewModel.cs
public class ManagePatientViewModel : ViewModelBase
{
public ManagePatientViewModel (MainWindowViewModel inMainVM) : base(inMainVM) {}
}
ViewHelper.cs
public enum ViewState
{
SEARCH,
CREATE,
}
MainWindowViewModel.cs
public ViewState State {get;set;}
public ManagePatientViewModel VM {get;set;}
private void ChangeView(ViewState inState)
{
State = inState;
// This is need to force the update of Content.
var copy = VM;
MainContent = null;
MainContent = copy;
}
public void NavigateTo (NavigationType inNavigation)
{
switch (inNavigationType)
{
case NavigationType.CREATE_PATIENT:
ChangeView(ViewState.CREATE);
break;
case NavigationType.SEARCH_PATIENT:
ChangeView(ViewState.SEARCH);
break;
default:
throw new ArgumentOutOfRangeException(nameof(inNavigationType), inNavigationType, null);
}
}
MainWindow.xaml
<DataTemplate x:Key="CreateTemplate">
<views:CreateView />
</DataTemplate>
<DataTemplate x:Key="SearchTemplate">
<views:SearchView/>
</DataTemplate>
<TemplateSelector x:Key="ViewSelector"
SearchViewTemplate="{StaticResource SearchTemplate}"
CreateViewTemplate="{StaticResource CreateTemplate}"/>
<ContentControl
Grid.Row="1"
Content="{Binding MainContent}"
ContentTemplateSelector="{StaticResource ViewSelector}" />
TemplateSelector.cs
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate SearchViewTemplate {get;set;}
public DataTemplate CreateViewTemplate {get;set;}
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (!(item is SelectLesionViewModel vm))
{
return null;
}
switch (vm.ViewType)
{
case ViewState.CREATE:
return CreateViewTemplate;
case ViewState.SEARCH:
return SearchViewTemplate;
default:
return null;
}
}
}
How is the TemplateSelector supposed to know which template to use when there are two view types mapped to a single view model type? This makes no sense I am afraid.
You should use two different types. You could implement the logic in a common base class and then define two marker types that simply derive from this implementation and add no functionality:
public class ManagePatientViewModel { */put all your code in this one*/ }
//marker types:
public class SearchPatientViewModel { }
public class CreatePatientViewModel { }
Also, you don't really need a template selector if you remove the x:Key attributes from the templates:
<DataTemplate DataType="{x:Type viewModels:SearchPatientViewModel}">
<views:SearchPatientView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:CreatePatientViewModel}">
<views:CreatePatientView />
</DataTemplate>
...
<ContentControl
Grid.Row="1"
Content="{Binding MainContent}" />
Maybe the requirement is to switch out the views and retain the one viewmodel.
Datatemplating is just one way to instantiate a view.
You could instead set the datacontext of the contentcontrol to the instance of your viewmodel and switch out views as the content. Since views are rather a view responsibility such tasks could be done completely in the view without "breaking" mvvm.
Here's a very quick and dirty approach illustrating what I mean.
I build two usercontrols, UC1 and UC2. These correspond to your various patient views.
Here's the markup for one:
<StackPanel>
<TextBlock Text="User Control ONE"/>
<TextBlock Text="{Binding HelloString}"/>
</StackPanel>
I create a trivial viewmodel.
public class OneViewModel
{
public string HelloString { get; set; } = "Hello from OneViewModel";
}
My mainwindow markup:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Button Content="UC1" Click="UC1_Click"/>
<Button Content="UC2" Click="UC2_Click"/>
</StackPanel>
<ContentControl Name="parent"
Grid.Column="1"
>
<ContentControl.DataContext>
<local:OneViewModel/>
</ContentControl.DataContext>
</ContentControl>
</Grid>
The click events switch out the content:
private void UC1_Click(object sender, RoutedEventArgs e)
{
parent.Content = new UC1();
}
private void UC2_Click(object sender, RoutedEventArgs e)
{
parent.Content = new UC2();
}
The single instance of oneviewmodel is retained and the view shown switches out. The hellostring binds and shows ok in both.
In your app you will want a more sophisticated approach to setting that datacontext but this sample is intended purely as a proof of concept to show you another approach.
Here's the working sample:
https://1drv.ms/u/s!AmPvL3r385QhgpgMZ4KgfMWUnxkRzA

Creating generalized user controls with MVVM Light

How to create a general user control using MVVM Light?
All the main views in the application seem to work fine. However, general controls doesn't seem to accept bindings. This is my FileDiplay control. An icon and a TextBlock displaying a filename next to it.
Utilization
In one of the main views, I try to bind a FileName inside an ItemsTemplate of an ItemsControl. Specifying a literal, like FileName="xxx" works fine, but binding doesn't.
<local:FileLink FileName="{Binding FileName}" />
I've been playing around with DependencyProperty and INotifyPropertyChanged a lot. And seemingly there's no way around a DependencyProperty, since it can't be bound otherwise. When using a simple TextBlock instead of this user control, binding is accepted.
I didn't include the locator or the utilizing control in order to avoid too much code. In fact, I think this is a very simple problem that I haven't found the solution for, yet. I do think that having the DataContext set to the ViewModel is correct, since no list binding or real UserControl separation is possible. I've also debugged into the setters and tried the different approaches.
FileLink.xaml
<local:UserControlBase
x:Class="....FileLink"
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:..."
mc:Ignorable="d" DataContext="{Binding FileLink, Source={StaticResource Locator}}">
<Grid>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Icon}" Margin="0,0,5,0" />
<TextBlock Text="{Binding FileName}" />
</StackPanel>
</Grid>
</local:UserControlBase>
FileLink.xaml.cs
using System.Windows;
using System.Windows.Media;
namespace ...
{
public partial class FileLink : UserControlBase
{
private FileLinkViewModel ViewModel => DataContext as FileLinkViewModel;
public static DependencyProperty FileNameProperty = DependencyProperty.Register(nameof(FileName), typeof(string), typeof(FileLink));
public ImageSource Icon
{
get
{
return App.GetResource("IconFileTypeCsv.png"); // TODO:...
}
}
public string FileName
{
get
{
return ViewModel.FileName;
}
set
{
ViewModel.FileName = value;
}
}
public FileLink()
{
InitializeComponent();
}
}
}
FileLinkViewModel.cs
using GalaSoft.MvvmLight;
namespace ...
{
public class FileLinkViewModel : ViewModelBase
{
private string _FileName;
public string FileName
{
get
{
return _FileName;
}
set
{
Set(() => FileName, ref _FileName, value);
}
}
}
}
Do not explicitly set the DataContext of your UserControl, because it effectively prevents that the control inherits the DataContext from its parent control, which is what you expect in a Binding like
<local:FileLink FileName="{Binding FileName}" />
Also, do not wrap the view model properties like you did with the FileName property. If the view model has a FileName property, the above binding works out of the box, without any wrapping of the view model.
If you really need a FileName property in the UserControl, it should be a regular dependency property
public partial class FileLink : UserControlBase
{
public FileLink()
{
InitializeComponent();
}
public static readonly DependencyProperty FileNameProperty =
DependencyProperty.Register(nameof(FileName), typeof(string), typeof(FileLink));
public string FileName
{
get { return (string)GetValue(FileNameProperty); }
set { SetValue(FileNameProperty, value); }
}
}
and you should bind to it by specifying the UserControl as RelativeSource:
<local:UserControlBase ...> <!-- no DataContext assignment -->
<StackPanel Orientation="Horizontal">
<Image Source="IconFileTypeCsv.png" Margin="0,0,5,0" />
<TextBlock Text="{Binding FileName,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
</local:UserControlBase>

WPF: How to bind a Visibility property to an xaml element?

I've got some problem I need some help with. I want to bind the visibility properties from a view model to the xaml elements so I get some visually changes (collapse or show in this case) by just changing the value in the viewmodel.
I got this xaml
<Window x:Class="PampelMuse.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:welcome="clr-namespace:PampelMuse.Views.Welcome"
xmlns:backend="clr-namespace:PampelMuse.Views.Backend"
xmlns:pampelMuse="clr-namespace:PampelMuse" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
Title="PampelMuse" Height="670" Width="864">
<Grid>
<Image HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Source="Resources/Images/Backgrounds/4.jpg" Stretch="UniformToFill" />
<welcome:WelcomeScreen x:Name="UIWelcome" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding ElementName=UiWelcomeVisibility}" />
<backend:BackendUI x:Name="UIBackend" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Visibility="{Binding ElementName=UiBackendVisibility}" />
</Grid>
The visibilities as you can see are binded to the properties UiWelcomeVisibility and UiBackendVisibility in the UIModel. These properties are now defined as followed:
public partial class MainWindow : Window
{
private ViewModel.ViewModel ViewModel = PampelMuse.ViewModel.ViewModel.GetInstance();
public MainWindow()
{
InitializeComponent();
DataContext = ViewModel; // Setting the data context what effects all the xaml elements in this component too, including UIWelcome and BackendUI
ViewModel.UIModel.UiBackendVisibility = Visibility.Collapsed;
}
The ViewModel:
public class ViewModel
{
private static ViewModel instance = new ViewModel();
public UIModel UIModel = UIModel.GetInstance();
public static ViewModel GetInstance()
{
return instance;
}
}
And the UIModel:
public class UIModel
{
private static UIModel instance = new UIModel();
public Visibility UiWelcomeVisibility { get; set; }
public Visibility UiBackendVisibility { get; set; }
public static UIModel GetInstance()
{
return instance;
}
}
I just don't see any coding mistakes here (and I don't get some at runtime in fact) but the BackendUI-visibility-property is not changed by the UiBackendVisibility of UIModel.
Any ideas? Thanks so far.
You are doing the binding wrong. Visibility="{Binding ElementName=UiWelcomeVisibility}" sets the visibility of an element equal to another visual element named "UiWelcomeVisibility". There are two problems with this:
There is no element named "UiWelcomeVisibility" in the first place.
Even if there were, a visual element itself is not a valid value for the Visibility property.
What you want is to databind to the viewmodel instead. Assuming that you have already set the DataContext to the viewmodel, just use
<welcome:WelcomeScreen ... Visibility="{Binding UiWelcomeVisibility}" />

Binding ViewModel to ContentControl as its DataContext

I want to change UserControls on button clicks (I'm not going to complicate here, so I'll only mention important parts). So idea was to bind ViewModels of those UserControls to ContentControl, and than associate them Views using DataTemplates.
Here's the code:
<Window x:Class="Project.MainWindow">
<Window.Resources>
<DataTemplate DataType="{x:Type UserControl:ViewUserControlViewModel}" >
<UserControl:ViewUserControl/>
</DataTemplate>
<DataTemplate DataType="{x:Type UserControl:EditUserControlViewModel}" >
<UserControl:EditUserControl/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl DataContext="{Binding UserControlViewModel}" />
<Button Content="View" Click="ChangeToView()"/>
<Button Content="Edit" Click="ChangeToEdit()"/>
</Grid>
</Window>
ViewModel:
public class MainWindowViewModel : DependencyObject
{
public DependencyObject UserControlViewModel
{
get { return (DependencyObject)GetValue(UserControlViewModelProperty); }
set { SetValue(UserControlViewModelProperty, value); }
}
public static readonly DependencyProperty UserControlViewModelProperty =
DependencyProperty.Register("UserControlViewModel", typeof(DependencyObject), typeof(MainWindowViewModel), new PropertyMetadata());
public MainWindowViewModel()
{
UserControlViewModel = new EditUserControlViewModel();
}
}
But theres a problem. When I start project, I only see buttons but not any UserControls. What did I do wrong?
If your Window.DataContext is properly set to MainWindowViewModel this should do the job
<ContentControl Content="{Binding UserControlViewModel}" />
When doing mvvm your viewmodel should implement INotifyPropertyChanged and not inherit from DependencyObject.
public class MainWindowViewModel : INotifyPropertyChanged
{
private object _currentWorkspace; //instead of object type you can use a base class or interface
public object CurrentWorkspace
{
get { return this._currentWorkspace; }
set { this._currentWorkspace = value; OnPropertyChanged("CurrentWorkspace"); }
}
public MainWindowViewModel()
{
CurrentWorkspace= new EditUserControlViewModel();
}
//todo: to switch the workspace, create DelegeCommand/RelayCommand and set the CurrentWorkspace
//if you don't know about these commands let me know and i post it
public ICommand SwitchToViewCommand {get{...}}
public ICommand SwitchToEditCommand {get{...}}
}
xaml: you should set the Content Property to your CurrentWorkspace.
<ContentPresenter Content="{Binding UserControlViewModel}" />
<Button Content="View" Comamnd="{Binding SwitchToViewCommand}"/>
<Button Content="Edit" Comamnd="{Binding SwitchToEditCommand}"/>
! Don't forget to set the DataContext for your window to your MainWindowViewModel instance.
First of all you should post the code of your UserControl since (in your code snippet above) it's responsible for displaying some data.
Second you are not binding anything in your code.
Third your implementation of the ViewModel is wrong. You don't need to subclass a DependencyObject but instead implement the INotifyPropertyChanged interface in order to establish a ViewModel that is capable of notifying your View.
Fourth I don't know what you are doing with
<ContentControl DataContext="{Binding UserControlViewModel}" />
maybe you can explain further ?
Fifth when implementing the MVVM patterm (what you currently not do) you should avoid using events like the click event and instead use Commands.
(I know that's not a real answer yet, but I don't wanted to write in comment syntax)

Categories