INotifyPropertyChanged and Button.IsEnabled - c#

I have View and its ViewModel. In View, I have some Panels that has binding to some properties in my ViewModel;
For example first property is SomeObject. In that panel I have some textboxes, that has binding to some properties in SomeObject class. All bindings in ViewModel and classes implements INPC.
That textbox, in panel, has binding to its property in SomeObject class this way: SomeObject.Property.
In that view I have a Save button. I need change the IsEnabled property of my button every time when something was entered in textboxes or changed in panel.
I've done it only for SomeObject property in my viewmodel by changing IsEnabled property bound to button from SomeObject's setter. But when I change something in properties of my SomeObject it doesn't call the setter of main SomeObject in ViewModel, and I can't change my IsEnabled property from SomeObject class.
<controls:ExpandableSettingsPanel Header="REFUND" IsExpanded="{Binding RefundCustomerConfigurationEnabled, Mode=TwoWay}" ..>
<StackPanel..>
<CheckBox Content="Allowed within reversal period" IsChecked="{Binding RefundCustomerConfiguration.IsAllowedWithinReversalPeriod, Mode=TwoWay}"/>
<TextBlock Style="{DynamicResource GrayTextBlockStyle}" Text="Minimal amount to be refunded"/>
<WrapPanel>
<TextBlock Text="€" .. />
<controls:MementoTextBox Text="{Binding RefundCustomerConfiguration.MinimalAmountToBeRefunded, Mode=TwoWay,
NotifyOnValidationError=True, StringFormat=\{0:N2\},
TargetNullValue='',
UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True,
ValidatesOnExceptions=True}" ..>
<i:Interaction.Behaviors>
<behaviors:TextBoxInputRegExBehavior
EmptyValue="0.00"
IgnoreSpace="True"
RegularExpression="^\d{1,2}(\.?\d{1,2})?$"
MaxLength="6"
/>
</i:Interaction.Behaviors>
</controls:MementoTextBox>
</WrapPanel>
<TextBlock Style="{DynamicResource GrayTextBlockStyle}" Text="Minimal bank statement line age" ../>
<controls:MementoTextBox Text="{Binding RefundCustomerConfiguration.MinimalBankStatementLineAge,
Mode=TwoWay,
NotifyOnValidationError=True,
ValidatesOnNotifyDataErrors=True,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnExceptions=True }" ..>
<i:Interaction.Behaviors>
<behaviors:TextBoxInputRegExBehavior
EmptyValue="0"
IgnoreSpace="True"
RegularExpression="^\d{1,3}$"
MaxLength="3"
/>
</i:Interaction.Behaviors>
</controls:MementoTextBox>
</StackPanel>
</controls:ExpandableSettingsPanel>
This is my XAML part.
public RefundCustomerConfiguration RefundCustomerConfiguration
{
get { return _refundCustomerConfiguration; }
set
{
SetProperty(ref _refundCustomerConfiguration, value);
OnPropertyChanged("RefundCustomerConfigurationEnabled");
}
}
[Required(ErrorMessage = "This field is required")]
public bool IsAllowedWithinReversalPeriod
{
get { return GetValue(() => IsAllowedWithinReversalPeriod); }
set
{
SetPropertyValue(value);
}
}
And this is my properties; First - in ViewModel. Second - in first property class.

Just bind button's visibility property to the Visibility method in the class which realize INotifyPropertyChanged interface as like this:
public Visibility btnVisible
{
get
{
if(<your condition>)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
}

Related

Behavior DependencyProperty not updating ViewModel when used within DataTemplate

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>

TextBox.LineCount and TextBox.GetLastVisibleLineIndex() is always -1 when using MVVM pattern on WPF

I have a TabControl in my View, and I dynamically add TabItems which contains a textbox as a content. And when want to get Line Count from Selected Item, it always returns -1, also with textbox.GetLastVisibleLineIndex(). Code is below:
My View:
<TabControl x:Name="tabControl" HorizontalAlignment="Left" Height="385" Margin="5,50,0,0" VerticalAlignment="Top" Width="740" ItemsSource="{Binding Tabs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{ Binding SelectedTab, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<TextBox
Text="{Binding Text, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,diag:PresentationTraceSources.TraceLevel=High }" AcceptsReturn="True" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyUp">
<cmd:EventToCommand Command="{Binding DataContext.TextChanged, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
My ViewModel:
TabItem tabItem = new TabItem();
tabItem.Header = mainModel.Header;
TextBox textBox = new TextBox();
textBox.Text = mainModel.TextFile;
tabItem.LayoutUpdated += (sender2, e2) => textBox_LayoutUpdated(sender2, e2);
textBox.LayoutUpdated += (sender3, e3) => textBox_LayoutUpdated(sender3, e3);
tabItem.Content = textBox;
Tabs.Add(tabItem);
SelectedTab = tabItem;
private void textBox_LayoutUpdated(object sender, EventArgs args)
{
lineCount = ((SelectedTab as TabItem).Content as TextBox).LineCount;
}
MainModel is my Model in MVVM.
My View.cs:
this.UpdateLayout();
TabItem tab = this.tabControl.SelectedItem as TabItem;
int index = ((this.tabControl.SelectedItem as TabItem).Content as TextBox).GetLastVisibleLineIndex();
Even here in View.cs is always -1;
I am new in WPF MVVM,
Thanks.
You have many issues with your current code. It seems that you actually missed the point of MVVM.
First of all, a view-model must not be aware of the view, nor any view-specific types (e.g. TabItem). A view-model is just a layer that adopts your model in such way that this model can be presented by the view. A view-model must not construct the view itself, as you do it in your example.
The reason why you get -1 is that the TextBox you add into the tab item will never be laid out, because you override the tab item's ContentTemplate.
There are some other things you're either doing wrong or they're unnecessary:
Binding for the tab control's ItemsSource must not be TwoWay, because the tab control itself will never update this property value. The UpdateSourceTrigger is not needed here for the same reason.
UpdateSourceTrigger is not needed for the SelectedItem binding, because it has by default the PropertyChanged mode
you have a command named TextChanged, but by convention it has to be named TextChangedCommand (with a Command postfix)
tabItem.LayoutUpdated += (sender2, e2) => textBox_LayoutUpdated(sender2, e2) is an overkill for an event subscription that creates an unnecessary lambda capturing this, use the method group syntax instead: tabItem.LayoutUpdated += textBox_LayoutUpdated
And now the true MVVM solution:
Suppose you have the item's view-model:
class Item : ViewModelBase
{
public Item(string header, string textFile)
{
Header = header;
this.textFile = textFile;
}
public string Header { get; }
private string textFile;
public string TextFile
{
get => textFile;
set { textFile = value; OnPropertyChanged(); }
}
private int lineCount;
public int LineCount
{
get => lineCount;
set { lineCount = value; OnPropertyChanged(); Debug.WriteLine("Line count is now: " + value); }
}
}
This view-model represents a single item that will be displayed as a tab item. But maybe this will be some other control in the future - actually you don't have to bother with that. The view-model has no idea which controls exactly will display the values. The view-model just provides these values in a convenient way.
So, the Header and the TextFile properties contain the model values. The LineCount property will be calculated by the view (more on that see below).
The main view-model for that would look like this:
class ViewModel : ViewModelBase
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
private Item selectedItem;
public Item SelectedItem
{
get => selectedItem;
set { selectedItem = value; OnPropertyChanged(); }
}
}
Note that the Items collection property is read-only. That means no one can change the reference to the collection, but the collection itself is not read-only. The SelectedItem reference may be updated however.
And now the important point: the LineCount property of the TextBox will also be updated, e.g. when the TextBox wraps the text and the control is resized. So we can't just calculate the lines count in view-model, we need to do this in the view.
However, the view-model, as we know, must not be aware of the view. What to do? In such cases a good developer prefers a Behavior from the System.Windows.Interactivity namespace.
Let's create a simple behavior that will monitor the LineCount of a TextBox:
class LineCountBehavior : Behavior<TextBox>
{
public int LineCount
{
get { return (int)GetValue(LineCountProperty); }
set { SetValue(LineCountProperty, value); }
}
public static readonly DependencyProperty LineCountProperty =
DependencyProperty.Register("LineCount", typeof(int), typeof(LineCountBehavior), new PropertyMetadata(0));
protected override void OnAttached()
{
AssociatedObject.LayoutUpdated += RefreshLineCount;
AssociatedObject.TextChanged += RefreshLineCount;
}
protected override void OnDetaching()
{
AssociatedObject.LayoutUpdated -= RefreshLineCount;
AssociatedObject.TextChanged -= RefreshLineCount;
}
private void RefreshLineCount(object sender, EventArgs e)
{
LineCount = AssociatedObject.LineCount;
}
}
Now we can attach this behavior to any TextBox and use the behavior's LineCount dependency property as a binding source. Here is a full XAML setup:
<TabControl ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<TabControl.ItemTemplate>
<DataTemplate DataType="local:Item">
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="local:Item">
<TextBox Text="{Binding TextFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" TextWrapping="Wrap">
<i:Interaction.Behaviors>
<local:LineCountBehavior LineCount="{Binding LineCount, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
</TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
So this is a 'clean' MVVM solution. Hope I could give you some insights.
BTW, I don't know why you need this line count in the view-model...

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.

OnPropertyChanged method is not firing

In WP8 app, i have few controls where i bind the foreground color which i am changing in the codebehind. But OnPropertyChanged is not firing when the user event happened.
I have defined this binding "ControlForeground" in my textblock and radiobutton data template controls in it. I am trying to change the Foreground color whenever user presses the button. But my new color assignment is not updating the UI. Anything i am missing here?
In XAML,
<TextBlock x:Name="lblTileColor" TextWrapping="Wrap" Text="Selected color:" Foreground="{Binding ControlForeground, Mode=TwoWay}"/>
<TextBlock x:Name="lblTileColor2" TextWrapping="Wrap" Text="App bg:" Foreground="{Binding ControlForeground, Mode=TwoWay}"/>
<RadioButton x:Name="accentColor" IsChecked="true" BorderBrush="White" Foreground="{Binding ControlForeground, Mode=TwoWay}">
<RadioButton.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Rectangle Width="25" Height="25" Fill="{StaticResource PhoneAccentBrush}"/>
<TextBlock Width="10"/>
<TextBlock x:Name="lblDefaultAccent" Text="Default accent color" Foreground="{Binding ControlForeground, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</RadioButton.ContentTemplate>
</RadioButton>
<Button x:name="UpdateColor" click="update_btn"/>
In C#,
public class ColorClass : INotifyPropertyChanged
{
private SolidColorBrush _ControlForeground;
public SolidColorBrush ControlForeground
{
get
{
return _ControlForeground;
}
set
{
_ControlForeground = value;
OnPropertyChanged("ControlForeground");
}
}
public ColorClass() { }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public class ColorPage:PhoneApplicationPage{
public ObservableCollection<ColorClass> TestCollection { get; private set; }
public void update_btn(object sender, EventArgs e){
TestCollection.Add(new ColorClass()
{
ControlForeground = new SolidColorBrush(Colors.Red)
});
}
}
For your 2nd problem (not being able to bind controls inside your data template), this is because these controls will use the data context of the their parent template not the data context of the page.
To fix this, you'll have to tell these controls the element name with the data context and give it full path of your property.
<TextBlock
x:Name="lblDefaultAccent"
Text="Default accent color"
Foreground="{Binding DataContext.ControlForeground,
ElementName=LayoutRoot, Mode=TwoWay}"/>
As you can see above you have to specify the element name. In case you bound this using this.DataContext = colorClass then the element name will be the name of the outer grid in your xaml, defaulted as LayoutRoot
You can only bind an ObservableCollection to controls which expect it, like a ListBox or LongListSelector. Additionally, adding a Brush to the TestCollection doesn't fire the non-functional notification since it doesn't call the setter of that property, just modifies the existing object.
Make TestCollection a type ColorClass and change the .Add stuff to just change the ColorClass.ControlForeground property and this should "just work."

Categories