Behavior DependencyProperty not updating ViewModel when used within DataTemplate - c#

I have a DependencyProperty in a Behavior which I am setting the value for in OnAttached().
I am then binding view model properties to this DependencyProperty with a Mode of OneWayToSource.
For some reason the bound view model property does not get updated by the OneWayToSource binding when done within a DataTemplate (the view model's setter is never invoked). In other circumstances it appears to work fine.
I'm getting no binding errors and I can see no indications of any exceptions, etc, and am at a loss as to what it is I am doing wrong.
The WPF Designer does show some errors, claiming either The member "TestPropertyValue" is not recognized or is not accessible or The property "TestPropertyValue was not found in type 'TestBehavior', depending on where you look. I am unsure if these are 'real' errors (as I've observed the WPF Designer does not seem to be entirely reliable in always showing genuine issues), and if they are, are whether they are related to this issue or another problem entirely.
If these Designer errors do relate to this issue I can only assume that I must have declared the DependencyProperty incorrectly. If that is the case I am unable to see where the mistakes are.
I have produced an example project that replicates the issue. The following code should suffice and can be added to any new WPF project with the name WpfBehaviorDependencyPropertyIssue001.
MainWindow.xaml
<Window x:Class="WpfBehaviorDependencyPropertyIssue001.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tb="clr-namespace:WpfBehaviorDependencyPropertyIssue001.Behaviors"
xmlns:vm="clr-namespace:WpfBehaviorDependencyPropertyIssue001.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<StackPanel>
<Label Content="{Binding TestPropertyValue, ElementName=OuterTestA}" Background="Cyan">
<b:Interaction.Behaviors>
<tb:TestBehavior x:Name="OuterTestA" TestPropertyValue="{Binding MainTestValueA, Mode=OneWayToSource}" />
</b:Interaction.Behaviors>
</Label>
<Label Content="{Binding MainTestValueA, Mode=OneWay}" Background="Orange" />
<Label Content="{Binding MainTestValueB, Mode=OneWay}" Background="MediumPurple" />
<DataGrid ItemsSource="{Binding Items}" RowDetailsVisibilityMode="Visible">
<b:Interaction.Behaviors>
<tb:TestBehavior x:Name="OuterTestB" TestPropertyValue="{Binding MainTestValueB, Mode=OneWayToSource}" />
</b:Interaction.Behaviors>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<StackPanel>
<Label Content="{Binding TestPropertyValue, ElementName=InnerTest}" Background="Cyan">
<b:Interaction.Behaviors>
<tb:TestBehavior x:Name="InnerTest" TestPropertyValue="{Binding ItemTestViewModelValue, Mode=OneWayToSource}" />
</b:Interaction.Behaviors>
</Label>
<Label Content="{Binding ItemTestViewModelValue, Mode=OneWay}" Background="Lime" />
</StackPanel>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</StackPanel>
</Window>
TestBehavior.cs
using Microsoft.Xaml.Behaviors;
using System.Windows;
namespace WpfBehaviorDependencyPropertyIssue001.Behaviors
{
public class TestBehavior : Behavior<UIElement>
{
public static DependencyProperty TestPropertyValueProperty { get; } = DependencyProperty.Register("TestPropertyValue", typeof(string), typeof(TestBehavior));
// Remember, these two are just for the XAML designer (or I guess if we manually invoked them for some reason).
public static string GetTestPropertyValue(DependencyObject dependencyObject) => (string)dependencyObject.GetValue(TestPropertyValueProperty);
public static void SetTestPropertyValue(DependencyObject dependencyObject, string value) => dependencyObject.SetValue(TestPropertyValueProperty, value);
protected override void OnAttached()
{
base.OnAttached();
SetValue(TestPropertyValueProperty, "Example");
}
}
}
ViewModelBase.cs
using System.ComponentModel;
namespace WpfBehaviorDependencyPropertyIssue001.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainViewModel.cs
using System.Collections.ObjectModel;
namespace WpfBehaviorDependencyPropertyIssue001.ViewModels
{
public class MainViewModel : ViewModelBase
{
public ObservableCollection<ItemViewModel> Items
{
get => _Items;
set
{
_Items = value;
OnPropertyChanged(nameof(Items));
}
}
private ObservableCollection<ItemViewModel> _Items;
public MainViewModel()
{
Items = new ObservableCollection<ItemViewModel>()
{
new ItemViewModel() { ItemName="Item 1" }
};
}
public string MainTestValueA
{
get => _MainTestValueA;
set
{
System.Diagnostics.Debug.WriteLine($"Setting {nameof(MainTestValueA)} to {(value != null ? $"\"{value}\"" : "null")}");
_MainTestValueA = value;
OnPropertyChanged(nameof(MainTestValueA));
}
}
private string _MainTestValueA;
public string MainTestValueB
{
get => _MainTestValueB;
set
{
System.Diagnostics.Debug.WriteLine($"Setting {nameof(MainTestValueB)} to {(value != null ? $"\"{value}\"" : "null")}");
_MainTestValueB = value;
OnPropertyChanged(nameof(MainTestValueB));
}
}
private string _MainTestValueB;
}
}
ItemViewModel.cs
namespace WpfBehaviorDependencyPropertyIssue001.ViewModels
{
public class ItemViewModel : ViewModelBase
{
public string ItemName
{
get => _ItemName;
set
{
_ItemName = value;
OnPropertyChanged(nameof(ItemName));
}
}
private string _ItemName;
public string ItemTestViewModelValue
{
get => _ItemTestViewModelValue;
set
{
System.Diagnostics.Debug.WriteLine($"Setting {nameof(ItemTestViewModelValue)} to {(value != null ? $"\"{value}\"" : "null")}");
_ItemTestViewModelValue = value;
OnPropertyChanged(nameof(ItemTestViewModelValue));
}
}
private string _ItemTestViewModelValue;
}
}
Expected Debug output messages (excluding the standard WPF ones):
Setting MainTestValueA to null
Setting MainTestValueA to "Example"
Setting MainTestValueB to null
Setting MainTestValueB to "Example"
Setting ItemTestViewModelValue to null
Setting ItemTestViewModelValue to "Example"
Actual Debug output messages (excluding the standard WPF ones):
Setting MainTestValueA to null
Setting MainTestValueA to "Example"
Setting MainTestValueB to null
Setting MainTestValueB to "Example"
Setting ItemTestViewModelValue to null

I tested your code completely and it works fine.
Your Debug works well because all members are called at once when an instance of the MainViewModel is created.
The MainTestValueA is called with a value of null, then OnPropertyChanged is called and bind to the label control is called with the TestPropertyValue property and with that OnAttached method which initializes the example and prints it on the output.
The same steps for MainTestValueB
And the same steps are repeated for ItemTestViewModelValue but because it is inside DataGridView clr does not allow access from View.
Of course, this is my conclusion.

I have managed to resolve the issue.
For some reason, it looks like an UpdateSourceTrigger of PropertyChanged is needed for a binding within a DataTemplate that has a Mode of OneWayToSource.
Doing this results in the view model property being updated correctly.
I discovered this through experimentation, and I am uncertain why this behaviour differs from the binding done outside the DataTemplate, although it is possible that this behaviour is documented somewhere.
If I can find the reason for this behaviour (documented or not) I will update this answer with that information.
Additional information
For clarity for future readers, the label with the OneWayToSource binding outside of the DataTemplate worked as expected. The XAML for this (from the original question) is shown below:
<Label Content="{Binding TestPropertyValue, ElementName=OuterTestA}" Background="Cyan">
<b:Interaction.Behaviors>
<tb:TestBehavior x:Name="OuterTestA" TestPropertyValue="{Binding MainTestValueA, Mode=OneWayToSource}" />
</b:Interaction.Behaviors>
</Label>
However, the TestBehavior with the OneWayToSource binding within the DataTemplate did not work. The XAML for this (from the original question) is shown below:
<DataTemplate>
<StackPanel>
<Label Content="{Binding TestPropertyValue, ElementName=InnerTest}" Background="Cyan">
<b:Interaction.Behaviors>
<tb:TestBehavior x:Name="InnerTest" TestPropertyValue="{Binding ItemTestViewModelValue, Mode=OneWayToSource}" />
</b:Interaction.Behaviors>
</Label>
<Label Content="{Binding ItemTestViewModelValue, Mode=OneWay}" Background="Lime" />
</StackPanel>
</DataTemplate>
Adding UpdateSourceTrigger=PropertyChanged to the TestBehavior binding resulted in the view model property being updated correctly. The updated XAML is shown below:
<DataTemplate>
<StackPanel>
<Label Content="{Binding TestPropertyValue, ElementName=InnerTest}" Background="Cyan">
<b:Interaction.Behaviors>
<tb:TestBehavior x:Name="InnerTest" TestPropertyValue="{Binding ItemTestViewModelValue, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}" />
</b:Interaction.Behaviors>
</Label>
<Label Content="{Binding ItemTestViewModelValue, Mode=OneWay}" Background="Lime" />
</StackPanel>
</DataTemplate>

Related

WPF switch multiple control templates with DataType property fails

I am trying to make a contactlist with 2 different types of contacts, FysiekContactPersoon (Fysical persons) and WinkelOfBedrijf (Corporates). They both are inherited from the class ContactPersoon.
my MainWindow.xaml.cs
public partial class MainWindow : Window
{
ContactPersoonViewModel _viewmod = null;
public ContactPersoonViewModel ViewMod
{
get { _viewmod ??= new ContactPersoonViewModel(); return _viewmod; }
set => _viewmod = value;
}
public MainWindow()
{
InitializeComponent();
ViewMod.Import();
DataContext = ViewMod;
}
private void InfoButton_Click(object sender, RoutedEventArgs e)
{
DialogInfo dlg = new DialogInfo(ViewMod) { Owner = this };
if (dlg.ShowDialog() == true) { }
}
}
When the user selects a contact from a datagrid on mainwindow and presses the Info button, the dialog window opens.I have created two templates that normally have to be applied each to its corresponding class.But the dialogwindow is empty, despite the fact that the current item is shown properly in the viewModel when debugging.
My dialoginfo.xaml (simplified):
<ContentControl DataContext="{Binding CurrentCP}" Content="{Binding}">
<ContentControl.Resources>
<DataTemplate DataType="x:Type local:FysiekeContactpersoon">
<StackPanel Margin="5,5,5,5" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Content="Person:" HorizontalAlignment="Center" VerticalAlignment="Center" Width="114" Height="26" />
<TextBox x:Name="ContactNaam" HorizontalAlignment="Center" TextWrapping="Wrap" Text="{Binding Naam}" VerticalAlignment="Center" Width="218" Height="22"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="x:Type local:WinkelOfBedrijf">
<StackPanel Margin="5,5,5,5" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Content="Corporate:" HorizontalAlignment="Center" VerticalAlignment="Center" Width="114" Height="26" />
<TextBox x:Name="ContactNaam" HorizontalAlignment="Center" TextWrapping="Wrap" Text="{Binding Naam}" VerticalAlignment="Center" Width="218" Height="22"/>
</StackPanel>
</DataTemplate>
</ContentControl.Resources>
and my dialoginfo.xaml.cs
public partial class DialogInfo : Window
{
ContactPersoonViewModel _viewModel = null;
public ContactPersoonViewModel ViewModel { get => _viewModel; set => _viewModel = value; }
public DialogInfo(ContactPersoonViewModel vm)
{
ViewModel = vm;
InitializeComponent();
DataContext = vm.CurrentCP;
}
What am I doing wrong here? I was through a lot of similar threads, mostly pointing at this solution as correct and the simplest one, comparing with DataTemplateSelector or Property setters and triggers (which also aren't working with me- I've tried :().
Moreover, I have each second time a compilation fail "The key had already been added" of something, but the next compilation is perfectly succeeded after no code has been changed at all(WTF??!).Needless to say, how disappointed I am in XAML. I would appreciate some help in the form of a piece of a suitable code, or a very good tutorial link.
It looks like your code should basically work. The only problem I found is you type declaration on the DataTemplate.
For properties of type Type like Style.TargetType the XAML engine will convert the string representation of a type to an actual Type instance.
But this is not the case for properties like DataTemplate.DataType. Since DataTemplate defines the property DataType of type object, there will be no internal conversion from string to Type.
This is because DataTemplate.DataType expects a string for XML types and Type for objects.
Because you assigned a string to DataTemplate.DataType, no object type is resolved, as the data object is expected to be a XML object.
Using x:Type in order to define a Type rather than a string is correct, but you simply forgot to mark the declaration as markup extension using curly braces! Without this braces you are just defining a string value.
The correct syntax is:
<DataTemplate DataType="{x:Type local:FysiekeContactpersoon}">
...
</DataTemplate>

Navigate through TabItem using MVVm

Assum that I have 3 user Control(TIShowNames,TIEnterCode,TIShowFactor).
they have their views and their corresponding viewModel.
all these 3, are in mainwindowView.
Here is my mainwindowView Xaml:
<Controls:TransitionPresenter Name="transContainer" Grid.Row="2" RestDuration="0:0:1" IsLooped="False" Transition="{StaticResource SlideTransition}">
<TabControl Name="TCMain" Background="#00FFFFFF" BorderThickness="0" Padding="0 -5 0 0 ">
<TabItem Name="TIShowNames" Visibility="Collapsed">
<views:NameView x:Name="NameViewElement" />
</TabItem>
<TabItem Name="TIEnterCode" Visibility="Collapsed">
<views:CodeView x:Name="CodeViewElement" />
</TabItem>
<TabItem Name="TIShowFactor" Visibility="Collapsed">
<views:FactorDetailView x:Name="FactorDetailViewElement" />
</TabItem>
</TabControl>
</Controls:TransitionPresenter>
In my old Programming style i used to use this line of code for navigating through tab items(without any pattern):
private void ChangeTabItemTo(TabItem TI)
{
transContainer.ApplyTransition("TCMain", "TCMain");
TCMain.SelectedItem = TI;
}
I have a btn show in "TIShowNames", so when i clicks on that it has to go to "TIShowFactor".
In MVVM, ViewModel does not know any thing about view(this item tab is in its parent view!!!). so how he can change selected Tab Item without violating MVVM??
Another Try:
Changing Selectedindex wont work because of this error:
"System.Windows.Data Error: 40 : BindingExpression path error: 'Index'
property not found on 'object' ''MainWindowViewModel'
(HashCode=22018304)'. BindingExpression:Path=AAA;
DataItem='MainWindowViewModel' (HashCode=22018304); target element is
'TabControl' (Name=''); target property is 'IsSelected' (type
'Boolean')"
Update:
Controls:TransitionPresenter is from Fluid DLL
Update:
I want to hide tab item's header so no one can click the header and navigatoin through header is possibe only via btns in usercontrols
You could define a DataTemplate per view model type in the view:
<TabControl Name="TCMain"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding ViewModel}"
Background="#00FFFFFF" BorderThickness="0" Padding="0 -5 0 0 ">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:NameViewViewModel}">
<views:NameView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:CodeViewViewModel}">
<views:CodeView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:FactorDetailViewModel}">
<views:FactorDetailView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
...and bind the SelectedItem property to a source property that you set in your view model, e.g.:
public object ViewModel
{
get { return _vm; }
set { _vm = value; NotifyPropertyChanged(); }
}
...
ViewModel = new CodeViewViewModel(); //displays the CodeView
Expanding on mm8's answer, this is how I'd do it:
First of all, I would create a BaseViewModel class to be inherited by every view model that will represent each tab of the TabControl.
I like to implement it as an abstract class with an abstract string property called "Title", so I can dynamically create the tabs and display their names (or titles). This class would also implement the NotifyPropertyChanged interface.
public abstract class BaseViewModel : INotifyPropertyChanged
{
public abstract string Title { get; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then I would create each view model inheriting from this base view model. for example:
public class NameViewModel : BaseViewModel
{
public override string Title
{
get
{
return "Name";
}
}
}
You would do the same for the other view models, only changing the "title" property of each of them.
Now I would create the MainView of the application and its corresponding view model.
The MainViewModel would have a collection of BaseViewModels and a "CurrentViewModel" (of type BaseViewModel) and would add all the view models you want to its collection on its constructor, like this:
public class MainViewModel : BaseViewModel
{
public override string Title
{
get
{
return "Main";
}
}
private ObservableCollection<BaseViewModel> _viewModels;
public ObservableCollection<BaseViewModel> ViewModels
{
get { return _viewModels; }
set
{
if (value != _viewModels)
{
_viewModels = value;
OnPropertyChanged();
}
}
}
private BaseViewModel _currentViewModel;
public BaseViewModel CurrentViewModel
{
get { return _currentViewModel; }
set
{
if (value != _currentViewModel)
{
_currentViewModel = value;
OnPropertyChanged();
}
}
}
public MainViewModel()
{
ViewModels = new ObservableCollection<BaseViewModel>();
ViewModels.Add(new NameViewModel());
ViewModels.Add(new CodeViewModel());
ViewModels.Add(new FactorDetailViewModel());
}
}
Finally, your main view would be similar to what mm8 posted:
(Notice the differences from my code to mm8's code: (1) You need to set the DisplayMemberPath of the TabControl to the "Title" property of the BaseViewModels and (2) You need to set the DataContext of the Window to your MainViewModel)
<Window ...>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="TCMain"
ItemsSource="{Binding ViewModels}"
DisplayMemberPath="Title"
SelectedItem="{Binding CurrentViewModel}"
Background="#00FFFFFF" BorderThickness="0" Padding="0 -5 0 0 ">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:NameViewModel}">
<local:NameView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:CodeViewModel}">
<local:CodeView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:FactorDetailViewModel}">
<local:FactorDetailView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Now it should work as expected. Everytime you change the active tab of the TabControl, the SelectedItem property of the control will change to the corresponding view model, which will be templated as its corresponding view.
This approach is called "View Model First" (instead of View First), by the way.
EDIT
If you want to have a button on one of the view models that has a command to change the current view model, this is how you do it:
I suppose you are familiarized with Josh Smith's RelayCommand. If you are not, just search for its implementation on the web.
You will need to create an ICommand property on your MainViewModel, which will be responsible to change the "CurrentViewModel" property:
private ICommand _showFactorDetailCommand;
public ICommand ShowFactorDetailCommand
{
get
{
if (_showFactorDetailCommand == null)
{
_showFactorDetailCommand = new RelayCommand(p => true, p => show());
}
return _showFactorDetailCommand;
}
}
private void show()
{
CurrentViewModel = ViewModels.Single(s => s.Title == "Factor");
}
The show() method above simply searches the collection of view models that has the title "Factor" and set it to the CurrentViewModel, which in turn will be the Content of the ContentControl that acts as the ContentTemplate of your TabControl inside your main view.
Remember that your FactorDetailViewModel should be implemented as follows:
public class FactorDetailViewModel : ViewModelBase
{
public override string Title
{
get
{
return "Factor";
}
}
}
The button inside your "NameView" will bind to this command which is a property of "MainViewModel" using RelativeSource binding:
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ShowFactorDetailCommand}" Content="Show Factor" Height="20" Width="60"/>
You could make this command more generic, passing the title of the view model you would like to navigate to as the command parameter:
private ICommand _showCommand;
public ICommand ShowCommand
{
get
{
if (_showCommand == null)
{
_showCommand = new RelayCommand(p => true, p => show(p));
}
return _showCommand;
}
}
private void show(p)
{
var vm = (string)p;
CurrentViewModel = ViewModels.Single(s => s.Title == vm);
}
Then on your views, pass the Command Parameter too:
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ShowCommand}" Content="Show Factor" CommandParameter="Factor" Height="20" Width="60"/>
Finally, to hide your TabItems completely, you need to set the ItemContainerStyle of your TabControl so that the Visibility of your TabItems has the value of "Collapsed".
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</TabControl.ItemContainerStyle>

Listview Binding Does Not Update Programmatic Changes

I've done some research on the topic, and while I've come across some possibilities, nothing has worked for me.
Details:
I'm working on a WPF app using an MVVM design pattern. In the ViewModel, I have a List of Notes, a class with a few properties (among them, Note). I've created a property, SelectedNote on the VM to hold the currently selected note.
In my View, I've bound a ListView control to the list QcNotes. I've bound a TextBox to the SelectedNote property. When I make changes to the TextBox, they are correctly reflected in the appropriate row of the ListView.
Problem:
I've include a RevertChanges command. This is a relatively simple command that undoes changes I've made to the note. It correctly updates the TextBox, and it actually updates the underlying list correctly, but the changes do not update the ListView itself. (Is it necessary to use an ObservableCollection in this circumstance? I've been asked to try and resolve the problem without doing so).
Attempted Fixes
I tried to call NotifyPropertyChanged("SelectedNote") and NotifyPropertyChanged("QcNotes") directly from within the call to RevertChanges, but that hasn't fixed the problem.
Any ideas?
XAML
<Window.DataContext>
<VM:MainProjectViewModel />
</Window.DataContext>
<Grid>
<StackPanel>
<ListView ItemsSource="{Binding QcNotes, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" x:Name="list" SelectedItem="{Binding SelectedNote}">
<ListView.View>
<GridView>
<GridViewColumn Header="Note" DisplayMemberBinding="{Binding Note}" />
</GridView>
</ListView.View>
</ListView>
<TextBox
Height="30"
HorizontalAlignment="Stretch"
Text="{Binding SelectedNote.Note, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Allow Edits" Command="{Binding ChangeStateToAllowEditsCommand}" />
<Button Content="Save Changes" Command="{Binding EditNoteCommand}" />
<Button Content="Revert Changes" Command="{Binding RevertChangesToNoteCommand}" />
</StackPanel>
</StackPanel>
</Grid>
ViewModel Code
public class MainViewModel : BaseViewModel
{
private QcNote selectedNote;
private string oldNoteForUpdating;
private VMState currentState;
private string noteInput;
private IList<QcNote> qcNotes;
public IList<QcNote> QcNotes
{
get
{
return qcNotes;
}
set
{
qcNotes = value;
NotifyPropertChanged();
}
}
public QcNote SelectedNote
{
get
{
return selectedNote;
}
set
{
selectedNote = value;
oldNoteForUpdating = SelectedNote.Note;
NotifyPropertChanged();
}
}
public VMState CurrentState
{
get
{
return currentState;
}
set
{
currentState = value;
NotifyPropertChanged();
}
}
public ICommand RevertChangesToNoteCommand
{
get
{
return new ActionCommand(o => RevertChangestoNote());
}
}
private void RevertChangestoNote()
{
QcNotes.First(q => q.Id == SelectedNote.Id).Note = oldNoteForUpdating;
SelectedNote.Note = oldNoteForUpdating;
NotifyPropertChanged("SelectedNote");
NotifyPropertChanged("QcNotes");
CurrentState = VMState.View;
}
I'll post an answer to my own question, but don't want to deter other from offering suggestions.
I implemented the INotifyPropertyChanged interface on my Models.QcNote class, and that resolved the issue. Initially, the interface was implemented exclusively on the ViewModel. In that case, NotifyPropertyChanged was only called when the QcNote object itself was changed, not when the properties of the object were changed.

How to separate View data and ViewModel data?

I need to manage UI specific parameters (View) and Application data (Model/ViewModel) separately, so I'm using the code-behind of the View for the first, and a separated class (prefixed ViewModel) for the later. This is an simplified version of what I have:
View (XAML)
<Window x:Class="UrSimulator.View.MyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MyView" Height="300" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding FirstColumnWidth}" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<Label>Width:</Label>
<TextBox Text="{Binding FirstColumnWidth}" IsReadOnly="True" Background="LightGray" />
</StackPanel>
<StackPanel Grid.Column="1">
<Label>First Column Width:</Label>
<TextBox Text="{Binding FirstColumnWidth}" />
<Label>View Model Data:</Label>
<TextBox Text="{Binding MyViewModel.PropertyFromVM}" />
<Label Content="{Binding MyViewModel.PropertyFromVM}" />
</StackPanel>
</Grid>
View (Code behind)
public partial class MyView : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private MyViewModel m_MyViewModel;
public MyViewModel MyViewModel
{
get { return m_MyViewModel; }
set
{
m_MyViewModel = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("MyViewModel"));
}
}
private GridLength m_FirstColumnWidth;
public GridLength FirstColumnWidth
{
get { return m_FirstColumnWidth; }
set
{
m_FirstColumnWidth = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("FirstColumnWidth"));
}
}
public MyView()
{
MyViewModel = new MyViewModel();
DataContext = this;
FirstColumnWidth = new GridLength(100);
InitializeComponent();
}
}
ViewModel
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string m_PropertyFromVM;
public string PropertyFromVM
{
get { return m_PropertyFromVM; }
set
{
m_PropertyFromVM = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("PropertyFromVM"));
}
}
public MyViewModel()
{
PropertyFromVM = "Some business data";
}
}
It works, but I find it cumbersome to use MyViewModel. on every binding that points to the VM.
Questions:
Is there another way to do this without using the prefix?
How should I write the binding for the UI (the width property) if instead of using this for the DataContext, I'd use:
DataContext = MyViewModel;
I'm doing everything wrong and this is not how it is intended to be?
Note: Forget about the converter needed for the Width, it works as long as the text is valid and is not my concern on the question.
DataContext = this;
Yuck... :)
Let the view model be the data context, and bind on your view's properties like this :
<Window x:Name="This" ...>
...
<SomeControl SomeProperty="{Binding MyViewProperty, ElementName=This}"/>
...
</Window>
Side note :
class MyView : Window, INotifyPropertyChanged
Why aren't your view's properties "dependency properties" if you inherit Window ?
This is one way of doing MVVM, but not a great choice as you are still using tightly coupled View objects.
The ideal is where you let WPF infer what View class to use by binding your ViewModel objects to the Content property of ContentPresenters and setting up DataTemplate entries for your ViewModel types.
That way, you don't even need to use a DataContext = blah statement in your code anywhere.
e.g. in the App.xaml or similar
<DataTemplate DataType={x:Type MyViewModel}>
<local:MyViewModelView/>
</DataTemplate>
... then in the Window/UserControl/XAML wherever you need it...
<ContentPresenter Content={Binding MyViewModelAsAProperty}/>
...which can be a DependencyProperty or a standard INotifyPropertyChanged enabled property on another ViewModel.
Add this codes on your MyViewModel Class
private MyView _ObjMyViewModel;
public MyView ObjMyViewModel
{
get { return _ObjMyViewModel; }
set
{
_ObjMyViewModel= value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ObjMyViewModel"));
}
}
And in XAML
<Window.DataContext>
<ViewModel:MyViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ObjMyViewModel.FirstColumnWidth}" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<Label>Width:</Label>
<TextBox Text="{Binding ObjMyViewModel.FirstColumnWidth}" IsReadOnly="True" Background="LightGray" />
</StackPanel>
<StackPanel Grid.Column="1">
<Label>First Column Width:</Label>
<TextBox Text="{Binding ObjMyViewModel.FirstColumnWidth}" />
<Label>View Model Data:</Label>
<TextBox Text="{Binding MyViewModel.PropertyFromVM}" />
<Label Content="{Binding MyViewModel.PropertyFromVM}" />
</StackPanel>
I hope its working..

Dynamic user control change - WPF

I'm developing an app in WPF and I need to change in runtime a content of a ContentControl depending than the user selected on ComboBox.
I have two UserControls and at my combo exists two itens, corresponding each one each.
First usercontrol:
<UserControl x:Class="Validator.RespView"
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"
mc:Ignorable="d"
d:DesignHeight="167" d:DesignWidth="366" Name="Resp">
<Grid>
<CheckBox Content="CheckBox" Height="16" HorizontalAlignment="Left" Margin="12,12,0,0" Name="checkBox1" VerticalAlignment="Top" />
<ListBox Height="112" HorizontalAlignment="Left" Margin="12,43,0,0" Name="listBox1" VerticalAlignment="Top" Width="168" />
<Calendar Height="170" HorizontalAlignment="Left" Margin="186,0,0,0" Name="calendar1" VerticalAlignment="Top" Width="180" />
</Grid>
Second usercontrol:
<UserControl x:Class="Validator.DownloadView"
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"
mc:Ignorable="d"
d:DesignHeight="76" d:DesignWidth="354" Name="Download">
<Grid>
<Label Content="States" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
<ComboBox Height="23" HorizontalAlignment="Left" Margin="12,35,0,0" Name="comboBox1" VerticalAlignment="Top" Width="120" />
<RadioButton Content="Last 48 hs" Height="16" HorizontalAlignment="Left" Margin="230,42,0,0" Name="rdbLast48" VerticalAlignment="Top" />
<Label Content="Kind:" Height="28" HorizontalAlignment="Left" Margin="164,12,0,0" Name="label2" VerticalAlignment="Top" />
<RadioButton Content="General" Height="16" HorizontalAlignment="Left" Margin="165,42,0,0" Name="rdbGeral" VerticalAlignment="Top" />
</Grid>
At MainWindowView.xaml
<Window x:Class="Validator.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:du="clr-namespace:Validator.Download"
xmlns:resp="clr-namespace:Validator.Resp"
Title="Validator" Height="452" Width="668"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<Window.Resources>
<DataTemplate DataType="{x:Type du:DownloadViewModel}">
<du:DownloadView/>
</DataTemplate>
<DataTemplate DataType="{x:Type resp:RespViewModel}">
<resp:RespView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ComboBox ItemsSource="{Binding Path=PagesName}"
SelectedValue="{Binding Path=CurrentPageName}"
HorizontalAlignment="Left" Margin="251,93,0,0"
Name="cmbType"
Width="187" VerticalAlignment="Top" Height="22"
SelectionChanged="cmbType_SelectionChanged_1" />
<ContentControl Content="{Binding CurrentPageViewModel}" Height="171" HorizontalAlignment="Left" Margin="251,121,0,0" Name="contentControl1" VerticalAlignment="Top" Width="383" />
</Grid>
</Window>
I assigned to the DataContext of the MainView, the viewmodel below:
public class MainWindowViewModel : ObservableObject
{
#region Fields
private ICommand _changePageCommand;
private ViewModelBase _currentPageViewModel;
private ObservableCollection<ViewModelBase> _pagesViewModel = new ObservableCollection<ViewModelBase>();
private readonly ObservableCollection<string> _pagesName = new ObservableCollection<string>();
private string _currentPageName = "";
#endregion
public MainWindowViewModel()
{
this.LoadUserControls();
_pagesName.Add("Download");
_pagesName.Add("Resp");
}
private void LoadUserControls()
{
Type type = this.GetType();
Assembly assembly = type.Assembly;
UserControl reso = (UserControl)assembly.CreateInstance("Validator.RespView");
UserControl download = (UserControl)assembly.CreateInstance("Validator.DownloadView");
_pagesViewModel.Add(new DownloadViewModel());
_pagesViewModel.Add(new RespViewModel());
}
#region Properties / Commands
public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
{
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p),
p => p is IPageViewModel);
}
return _changePageCommand;
}
}
public ObservableCollection<string> PagesName
{
get { return _pagesName; }
}
public string CurrentPageName
{
get
{
return _currentPageName;
}
set
{
if (_currentPageName != value)
{
_currentPageName = value;
OnPropertyChanged("CurrentPageName");
}
}
}
public ViewModelBase CurrentPageViewModel
{
get
{
return _currentPageViewModel;
}
set
{
if (_currentPageViewModel != value)
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
#endregion
#region Methods
private void ChangeViewModel(IPageViewModel viewModel)
{
int indexCurrentView = _pagesViewModel.IndexOf(CurrentPageViewModel);
indexCurrentView = (indexCurrentView == (_pagesViewModel.Count - 1)) ? 0 : indexCurrentView + 1;
CurrentPageViewModel = _pagesViewModel[indexCurrentView];
}
#endregion
}
On MainWindowView.xaml.cs, I wrote this event to do the effective change:
private void cmbType_SelectionChanged_1(object sender, SelectionChangedEventArgs e)
{
MainWindowViewModel element = this.DataContext as MainWindowViewModel;
if (element != null)
{
ICommand command = element.ChangePageCommand;
command.Execute(null);
}
}
The app run ok and I inspected the application with WPFInspector and saw that the view changes when the combobox is changed internally, but the ContentControl still empty visually..
Sorry about the amount of code that I posted and my miss of knowledge but I'm working with this a long time and can't solve this problem.
Thanks
Issues:
Firstly don't ever create View related stuff in the ViewModel (UserControl). This is no longer MVVM when you do that.
Derive ViewModels from ViewModelBase and not ObservableObject unless you have a compelling reason to not use ViewModelBase when using MVVMLight. Keep ObservableObject inheritence for Models. Serves as a nice separation between VM's and M's
Next you do not need to make everything an ObservableCollection<T> like your _pagesViewModel. You do not have that bound to anything in your View's so it's just a waste. Just keep that as a private List or array. Check what a type actually does in difference to a similar other one.
Not sure about this one, maybe you pulled this code snippet as a demo, but do not use margins to separate items in a Grid. Your Layout is essentially just 1 Grid cell and the margins have the items not overlap. If you're not aware of that issue, Check into WPF Layout Articles.
Please don't forget principles of OOP, Encapsulation and sorts when writing a UI app. When having Properties like CurrentPageViewModel which you don't intend the View to switch make the property setter private to enforce that.
Don't resort to code-behind in the View too soon. Firstly check if it's only a View related concern before doing so. Am talking about your ComboBox SelectionChanged event handler. Your purpose of that in this demo is to switch the Bound ViewModel which is held in the VM. Hence it's not something that the View is solely responsible for. Thus look for a VM involved approach.
Solution:
You can get a working example of your code with the fixes for above from Here and try it out yourself.
Points 1 -> 5 are just basic straightforward changes.
For 6, I've created a SelectedVMIndex property in the MainViewModel which is bound to the SelectedIndex of the ComboBox. Thus when the selected index flips, the property setter after updating itself updates the CurrentPageViewModel as well such as
public int SelectedVMIndex {
get {
return _selectedVMIndex;
}
set {
if (_selectedVMIndex == value) {
return;
}
_selectedVMIndex = value;
RaisePropertyChanged(() => SelectedVMIndex);
CurrentPageViewModel = _pagesViewModel[_selectedVMIndex];
}
}

Categories