I was working on a project when I came across an issue with RaisePropertyChanged from MVVM Light that I can't seem to figure out. When I try to raise a change for my list, the list does get updated, but as does the selected index value above. The value that is passed to my selected index appears to be influenced by what key was pressed to trigger the event (i.e. if I press "BACKSPACE", the value passed to the setter is "-1", whereas if I enter a letter, the value passed is "0")
I recreated a project which purely demonstrates the issue. Below is the main bit of logic found in the MainVeiwModel:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
_testItems = new List<TestItem>()
{
new TestItem() { Name = "Test1" },
new TestItem() { Name = "Test2" }
};
}
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
RaisePropertyChanged("SelectedIndex");
RaisePropertyChanged("SelectedText");
RaisePropertyChanged("TestList");
}
}
public string SelectedText
{
get
{
return _testItems[_selectedIndex].Name;
}
set
{
_testItems[_selectedIndex].Name = value;
RaisePropertyChanged("TextList");
}
}
public List<string> TextList
{
get
{
_textList = new List<string>();
if (_testItems != null && _testItems.Count > 0)
{
foreach (TestItem item in _testItems)
_textList.Add(item.Name);
}
return _textList;
}
set { _textList = value; }
}
private int _selectedIndex;
private List<string> _textList;
private List<TestItem> _testItems;
}
My XAML:
<Window x:Class="RaisePropertyBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:local="clr-namespace:RaisePropertyBug"
mc:Ignorable="d"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding TextList, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Text="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
For context: I have a ComboBox which lists the names from a collection of items I have. There is an edit window where users can change names and other properties of these items. My goal is to have the ComboBox list update as the user edits the value. In my actual program, you are able to do this with the item at index 0, but any other index will automatically change to 0 as soon as a key is pressed and the RaisePropertyChanged() area is reached.
Check below code if it's working as per your requirement.
Use SelectedItem property of ComboBox and bind selecteditem to the edit screen/textbox. I've bind SelectedTestItem.Name propety here.
View -
<Window x:Class="StackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:local="clr-namespace:StackOverflow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding TestItems, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedValuePath="Name"
SelectedItem="{Binding SelectedTestItem, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Text="{Binding SelectedTestItem.Name, UpdateSourceTrigger=PropertyChanged}" Width="200"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
View.cs -
public partial class MainWindow : Window, INotifyPropertyChanged
{
private TestItem selectedTestItem;
public TestItem SelectedTestItem
{
get { return selectedTestItem; }
set
{
selectedTestItem = value;
RaisePropertyChanged("SelectedTestItem");
}
}
public List<TestItem> TestItems
{
get;
set;
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
var items = new List<TestItem>()
{
new TestItem() { Name = "Test1" },
new TestItem() { Name = "Test2" }
};
TestItems = items;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
You don't even need INotifyPropertyChanged for this example. I'm not entirely certain of what you are trying to achieve, but this code will achieve what I have gleaned from your post.
<Window x:Class="RaisePropertyChangedExample.BindingExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Correct View" Width="150" Height="80">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Items}"
x:Name="ItemViews"
HorizontalAlignment="Stretch" VerticalAlignment="Center" DisplayMemberPath="Name"/>
<TextBox DataContext="{Binding SelectedItem, ElementName=ItemViews}" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
</StackPanel>
</Window>
and the supporting code
using System.Windows;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace RaisePropertyChangedExample
{
public partial class BindingExample : Window
{
public BindingExample()
{
InitializeComponent();
DataContext = new BindingExampleViewModel();
}
}
public class BindingExampleViewModel
{
public ObservableCollection<TestItemViewModel> Items { get; set; }
= new ObservableCollection<TestItemViewModel>(new List<TestItemViewModel>
{
new TestItemViewModel {Name = "Test1"},
new TestItemViewModel {Name = "Test2"}
});
}
public class TestItemViewModel
{
public string Name { get; set; }
}
}
Unless there is some need of the index of the selected Item, there is no real argument against simply exposing each item as a TestItemViewModel view model and binding the other controls directly to the selected item itself. If however other controls are bound to the members of the TestItemViewModel then it's still not necessarily true that you should implement INotifyPropertyChanged on that view model.
The following example will still display the correct information when wired up with the existing ViewModel:
<Window x:Class="RaisePropertyChangedExample.BindingExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Correct View" Width="150" Height="100">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Items}"
x:Name="Combo"
HorizontalAlignment="Stretch" VerticalAlignment="Center" DisplayMemberPath="Name"/>
<Grid DataContext="{Binding SelectedItem, ElementName=Combo}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<Label Grid.Row="1" HorizontalAlignment="Stretch" Height="20" Content="{Binding Name}" />
</Grid>
</StackPanel>
</Window>
Normally
Updating after every keystroke can diminish performance and it denies the user the usual opportunity to backspace and fix typing errors before committing to the new value. see MS reference
however, this is only an issue if other processing occurs as the result of updating the source. If you are worried about the amount of processing involved you can switch to the default behaviour of LostFocus by simply omitting `UpdateSourceTrigger' declaration.
Related
All, I am having a lot of problems with displaying a custom tooltip for my LiveCharts control Here is my code and what I have tried.
Chart C# Code:
public partial class Chart : UserControl
{
public ChartData chartData { get; set; }
public Chart()
{
chartData = new ChartData();
InitializeComponent();
Loaded += Control_Loaded;
}
private void Control_Loaded(object sender, RoutedEventArgs e)
{
// build here
DataContext = this;
chartData.Formatter = value => value.ToString("C", CultureInfo.CurrentCulture);
}
}
Chart XAML:
<UserControl x:Class="LandlordTenantDatabaseWPF.Chart"
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:LandlordTenantDatabaseWPF"
xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:Chart}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock x:Name="txtTitle" Grid.Row="0" Text="{Binding chartData.Title}" Foreground="#FF4B52E4" FontSize="36" TextAlignment="Center"/>
<lvc:CartesianChart x:Name="chart" Grid.Row="1" Series="{Binding chartData.SeriesCollection}" LegendLocation="Right" >
<lvc:CartesianChart.AxisX>
<lvc:Axis Title="Month" Labels="{Binding chartData.Labels}"></lvc:Axis>
</lvc:CartesianChart.AxisX>
<lvc:CartesianChart.AxisY>
<lvc:Axis Title="Payments" LabelFormatter="{Binding chartData.Formatter}"></lvc:Axis>
</lvc:CartesianChart.AxisY>
<lvc:CartesianChart.DataTooltip>
<local:CustomersTooltip/>
</lvc:CartesianChart.DataTooltip>
</lvc:CartesianChart>
</Grid>
ToolTip C# Code:
public partial class CustomersTooltip : IChartTooltip
{
private TooltipData _data;
public CustomersTooltip()
{
InitializeComponent();
//LiveCharts will inject the tooltip data in the Data property
//your job is only to display this data as required
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public TooltipData Data
{
get { return _data; }
set
{
_data = value;
OnPropertyChanged("Data");
}
}
public TooltipSelectionMode? SelectionMode { get; set; }
protected virtual void OnPropertyChanged(string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ToolTip XAML:
<UserControl x:Class="LandlordTenantDatabaseWPF.CustomersTooltip"
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:wpf="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
xmlns:local="clr-namespace:LandlordTenantDatabaseWPF"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance local:CustomersTooltip}"
Background="#E4555555" Padding="20 10" BorderThickness="2" BorderBrush="#555555">
<ItemsControl ItemsSource="{Binding Data.Points}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type wpf:DataPointViewModel}">
<Grid Margin="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="Title"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="Phone"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="PurchasedItems"/>
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0" Stroke="{Binding Series.Stroke}" Fill="{Binding Series.Fill}"
Height="15" Width="15"></Rectangle>
<TextBlock Grid.Column="1" Text="{Binding ChartPoint.Instance.(local:CustomerVm.Name)}"
Margin="5 0 0 0" VerticalAlignment="Center" Foreground="White"/>
<TextBlock Grid.Column="2" Text="{Binding ChartPoint.Instance.(local:CustomerVm.LastName)}"
Margin="5 0 0 0" VerticalAlignment="Center" Foreground="White"/>
<TextBlock Grid.Column="3" Text="{Binding ChartPoint.Instance.(local:CustomerVm.Phone),
StringFormat=Phone: {0}}"
Margin="5 0 0 0" VerticalAlignment="Center" Foreground="White"/>
<TextBlock Grid.Column="4" Text="{Binding ChartPoint.Instance.(local:CustomerVm.PurchasedItems),
StringFormat=Purchased Items: {0:N}}"
Margin="5 0 0 0" VerticalAlignment="Center" Foreground="White"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
ChartData Class:
public class ChartData : INotifyPropertyChanged
{
private SeriesCollection seriesCollection;
public SeriesCollection SeriesCollection
{
get { return seriesCollection; }
set
{
if (seriesCollection != value)
{
seriesCollection = value;
OnPropertyChanged("seriesCollection");
}
}
}
private string[] labels;
public string[] Labels
{
get { return labels; }
set
{
if(labels != value)
{
labels = value;
OnPropertyChanged("labels");
}
}
}
private string title;
public string Title
{
get { return title; }
set
{
if(title != value)
{
title = value;
OnPropertyChanged("title");
}
}
}
private Func<double, string> formatter;
public Func<double, string> Formatter
{
get { return formatter; }
set
{
if(formatter != value)
{
formatter = value;
OnPropertyChanged("formatter");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And the Window Containing the Chart:
private void AddNewSeries(List<double> lstPayments, string sSeriesName, SolidColorBrush colorBrush)
{
if (m_SeriesCollection == null)
{
m_SeriesCollection = new SeriesCollection
{ new ColumnSeries
{
Values = new ChartValues<double>(lstPayments),
Title = sSeriesName,
Fill = colorBrush
}
};
}
else
{
m_SeriesCollection.Add(
new ColumnSeries
{
Values = new ChartValues<double>(lstPayments),
Title = sSeriesName,
Fill = colorBrush
}
);
}
}
private void UpdateChartsBasedOnDates(Chart chart, string sChartTitle)
{ //Pass data to the chart
chart.chartData.SeriesCollection = m_SeriesCollection;
chart.chartData.Labels = GetFormattedDates(m_lstCombinedDates);
chart.chartData.Title = sChartTitle;
}
This all works perfectly when I just allow the default tooltip to be displayed on the chart. I am hung up on the fact that the chart data is supplied by a SeriesCollection, but the custom ToolTip, in the example on the LiveCharts website https://lvcharts.net/App/examples/v1/wpf/Tooltips%20and%20Legends doesn't use the SeriesCollection. Some of the charts displayed by my custom chart control have more than 1 series. I tried binding the Tooltip control to the SeriesCollection to get the data values, but that didn't work. Granted I may have done it incorrectly. Obviously the ToolTip XAML I am showing here is from the example code on the LiveCharts website, but I don't know where to go from here.
Is it possible to use make the tooltip use the series collection? What would be the best way for me to either use the Series Collection, or the ChartData class, or can I use
public TooltipData Data
{
get { return _data; }
set
{
_data = value;
OnPropertyChanged("Data");
}
}
from the ToolTip C# code to display the data in the ToolTip?
I'm so confused.
Edit: I forgot to mention what my goal is. I want to display a tooltip that says
string.Format("{0} Payment: ${1}", SeriesName, PaymentAmount);
This would display Rent Payment: $1000. or MortgagePayment: $1000.00
Without being a deep LiveCharts Tooltip Wiz I did manage to get a version going that worked for me. My chart is also using a SeriesCollection, so maybe this can be helpful for you.
This is how I integrated it into my chart:
CartesianChart cartesianChart = ...;
_cartesianChart.DataTooltip = new CustomTooltip
{
SelectionMode = TooltipSelectionMode.OnlySender
};
This is my CustomToolTip class (I believe this is pretty much straight out of the documentation somewhere):
public partial class CustomTooltip : UserControl, IChartTooltip
{
private TooltipData _data;
public CustomTooltip()
{
InitializeComponent();
//LiveCharts will inject the tooltip data in the Data property
//your job is only to display this data as required
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public TooltipData Data
{
get => _data;
set
{
_data = value;
OnPropertyChanged("Data");
}
}
public TooltipSelectionMode? SelectionMode { get; set; }
protected virtual void OnPropertyChanged(string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public partial class CustomTooltip : UserControl, IChartTooltip, INotifyPropertyChanged
{
private bool _contentLoaded;
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
public void InitializeComponent()
{
if (_contentLoaded)
return;
System.Uri resourceLocater = new System.Uri(
"/Plotting/CustomTooltipDesign.xaml",
System.UriKind.Relative
);
System.Windows.Application.LoadComponent(this, resourceLocater);
_contentLoaded = true;
}
}
Then I built a XAML component, just like you. First super simple:
public partial class CustomTooltipDesign : UserControl
{
public CustomTooltipDesign()
{
DataContext = this;
}
}
Then I had a little more effort to build the actual tooltip:
<UserControl x:Class="expomrf4utility.CustomTooltipDesign"
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:expomrf4utility="clr-namespace:ExpoMRF4Utility"
xmlns:wpf="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance expomrf4utility:CustomTooltip}"
x:Name="Control" d:DesignWidth="1" d:DesignHeight="1">
<UserControl.Resources>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{Binding Foreground}"></Setter>
</Style>
<wpf:SharedConverter x:Key="SharedConverter"/>
<wpf:SharedVisibilityConverter x:Key="SharedVisibilityConverter"/>
<wpf:ChartPointLabelConverter x:Key="ChartPointLabelConverter"/>
<wpf:ParticipationVisibilityConverter x:Key="ParticipationVisibilityConverter"/>
<BooleanToVisibilityConverter x:Key="Bvc"></BooleanToVisibilityConverter>
<CollectionViewSource x:Key="GroupedPoints" Source="{Binding Data.Points}"/>
</UserControl.Resources>
<UserControl.Template>
<ControlTemplate>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" >
<StackPanel Orientation="Vertical">
<ItemsControl ItemsSource="{Binding Source={StaticResource GroupedPoints}}" Grid.IsSharedSizeScope="True">
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type wpf:DataPointViewModel}">
<Grid Margin="2" VerticalAlignment="Center" HorizontalAlignment="Center" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" SharedSizeGroup="Title"/>
<RowDefinition Height="Auto" SharedSizeGroup="Label"/>
<RowDefinition Height="Auto" SharedSizeGroup="Label"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="1" Text="{Binding Series.Title}" HorizontalAlignment="Left" FontWeight="Bold"/>
<TextBlock Grid.Row="2" Text="{Binding ChartPoint.Y, StringFormat='Value: {0} V/m'}" HorizontalAlignment="Left"/>
<TextBlock Grid.Row="3" Text="{Binding ChartPoint, Converter={StaticResource ChartPointLabelConverter}, StringFormat='Datetime: {0}'}" HorizontalAlignment="Left"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</ControlTemplate>
</UserControl.Template>
Note how I had to create bindings for the different properties that I wanted to display. To that end I slightly cheated when creating the data sets themselves. I used GLineSeries but I have a feeling it should work with other series as well:
int bandIndex = ...;
GearedValues<double> gearedValues = (some double[] data).AsGearedValues();
series.Add(new GLineSeries
{
Values = gearedValues,
LabelPoint = chartPoint => loggerData.Labels[(int)chartPoint.X].ToString(),
Tag = bandIndex,
Title = bandIndex.ToString()
});
Note that I chartPoint.X is the (zero-based) index of the the specific point (in X) - whereas chartPoint.Y is the Y-value of the point (also used in the XAML). Using this index I can look up in my list of names loggerData.Labels and return the timestamp associated with this index.
I'm trying to figure out how to bind a property from my MainWindowViewModel to a ContentControl that is based on another View.
RelativeSource Binding seems not to work since the View is in another xaml file?
I tried with dependancy property but I didn't understand how to do this correctly I guess.
What I want to achieve here is that when I type in the TextBox of the MainWindow that all 3 views (contentcontrols) also view the updated data. It should be a demo to illustrate that in MVVM the ViewModel can change without knowledge of the Views and different Views react to it.
Sadly RelativeSource Binding and DependancyProperty didn't work for me or I missed a point.
MainWindow.xaml
<Window x:Class="MVVMDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<TextBlock Text="MVVM Demo" HorizontalAlignment="Center" FontSize="20" FontWeight="Bold"/>
<TextBox Text="{Binding TestString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200" Background="Black" Foreground="White" Margin="0 20 0 0"/>
<Grid Margin="0 20 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<ContentControl Grid.Column="0" Grid.Row="0" Content="{Binding ViewOne}"/>
<ContentControl Grid.Column="1" Grid.Row="0" Content="{Binding ViewTwo}"/>
<ContentControl Grid.Column="2" Grid.Row="0" Content="{Binding ViewThree}"/>
</Grid>
</StackPanel>
</Window>
MainWindowViewModel
public class MainWindowViewModel : ViewModelBase
{
/// <summary>
/// String to change the reaction of views
/// </summary>
private string _TestString;
public string TestString
{
get { return _TestString; }
set { _TestString = value; NotifyPropertyChanged(); }
}
private object _ViewOne { get; set; }
public object ViewOne
{
get { return _ViewOne; }
set { _ViewOne = value; NotifyPropertyChanged(); }
}
private object _ViewTwo { get; set; }
public object ViewTwo
{
get { return _ViewTwo; }
set { _ViewTwo = value; NotifyPropertyChanged(); }
}
private object _ViewThree { get; set; }
public object ViewThree
{
get { return _ViewThree; }
set { _ViewThree = value; NotifyPropertyChanged(); }
}
public MainWindowViewModel()
{
ViewOne = new ViewOne();
ViewTwo = new ViewTwo();
ViewThree = new ViewThree();
TestString = "ABC";
}
}
ViewOneViewModel
<UserControl x:Class="MVVMDemo.Views.ViewOne"
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:MVVMDemo.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="White">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=TestString, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" Foreground="Black"/>
<TextBlock Text="{Binding TestStringProperty}" Foreground="Black"/>
</StackPanel>
</Grid>
</UserControl>
ViewOneViewModel
public class ViewOneViewModel : ViewModelBase
{
// this doesn't help
public static readonly DependencyProperty TestStringProperty =
DependencyProperty.Register("TestString", typeof(string), typeof(MainWindowViewModel), new PropertyMetadata(null));
}
I believe your issue lies here:
<TextBlock Text="{Binding Path=TestString, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" Foreground="Black"/>
Binding Path=TestString should instead be Binding Path=DataContext.TestString as the RelativeSource is looking at the window now, not the window's model.
I would like to add, I have searched on Google, and SO for an answer to this before I posted, but none of the solutions seemed to work for me.
I am trying to select a "Person" from a WPF listbox, and have a property on my viewmodel updated to the selected "Person". It works fine the very first time I select an item. After the first selection, changing to another item does not update the viewmodel "Person".
The ViewModel:
public class PersonViewModel : BaseViewModel
{
private Person _selectedPerson;
private ICommand _addToPeopleCommand;
private ICommand _removeFromPeopleCommand;
public Person NewPerson { get; set; }
public Person SelectedPerson
{
get { return _selectedPerson; }
set
{
_selectedPerson = value;
OnPropertyChanged(nameof(SelectedPerson));
}
}
public ObservableCollection<Person> PeopleList { get; set; }
public ICommand AddToPeopleList
{
get
{
if (_addToPeopleCommand == null)
{
_addToPeopleCommand = new RelayCommand(
p => true,
p => this.PeopleList.Add(this.NewPerson));
}
return _addToPeopleCommand;
}
}
public ICommand RemoveFromPeopleList
{
get
{
if (_removeFromPeopleCommand == null)
{
_removeFromPeopleCommand = new RelayCommand(
p => true,
p => this.PeopleList.Remove(SelectedPerson));
}
return _removeFromPeopleCommand;
}
}
public PersonViewModel()
{
this.NewPerson = new Person();
this.PeopleList = new ObservableCollection<Person>();
}
}
BaseViewModel implements INotifyPropertyChanged. In case anyone is wondering.
And the XAML:
<Window x:Class="MVVM_Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:viewModels="clr-namespace:MVVM_Test"
d:DataContext="{d:DesignInstance viewModels:PersonViewModel}"
mc:Ignorable="d"
Title="MainWindow" Height="397" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding NewPerson.FirstName}"
Width="200"
Grid.Row="0"/>
<TextBox Text="{Binding NewPerson.LastName}"
Width="200"
Grid.Row="1"/>
<TextBox Text="{Binding NewPerson.FullName}"
Width="200"
Grid.Row="2"
IsReadOnly="True"/>
<Button Grid.Row="3"
Content="Add"
Margin="50,10,151,10"
Command="{Binding AddToPeopleList}"/>
<ListBox x:Name="listBox"
Margin="10,11,-206,-257"
ItemsSource="{Binding PeopleList}"
SelectedItem="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged}"
Grid.Row="4">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FullName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="remove" Content="Remove"
HorizontalAlignment="Left"
Margin="155,10,0,0"
Grid.Row="3"
VerticalAlignment="Top"
Width="75"
RenderTransformOrigin="0.067,0.9"
Command="{Binding RemoveFromPeopleList}"
CommandParameter="{Binding ElementName=listBox, Path=SelectedPerson}"/>
<TextBlock x:Name="textBlock"
HorizontalAlignment="Left"
Margin="311,15,-206,0"
TextWrapping="Wrap"
Text="{Binding SelectedPerson.FullName}"
VerticalAlignment="Top"
Width="196"
Height="75"
Grid.RowSpan="4"/>
</Grid>
The end goal is to select a person from the listbox and have it removed from the ObservableCollection. I can remove people from the box up until there is only one person left, at which point it stops working. However, as mentioned above, when I select a different person, after having removed one from the list, it no longer will select anyone to remove. Nor will selecting a second person call the setter a second time. I even tested it by including the TextBlock and binding it to the selected person property. It only updates once, the first time.
Your AddToPeopleList command adds the same Person instance again and again.
Changing the method to add a copy of NewPerson to the list may fix your problem.
I have a problem with bindings for DataTemplate based on defined DataType in ItemsControl, when I want to bind to my custom user control.
For demonstration purposes, I've created simple Item class example, where I have collection of items like this:
public class Item
{
public string ItemNameToBeSureWhatPropertyIsBound { get; set; }
}
In my ViewModel I create such collection, and expose it (with one item for comparison separately):
public class MainWindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<Item> _items;
private Item _exampleItem;
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new[] { new Item { ItemNameToBeSureWhatPropertyIsBound = "Me" }, new Item { ItemNameToBeSureWhatPropertyIsBound = "MySelf" }, new Item { ItemNameToBeSureWhatPropertyIsBound = "Ich" }, });
ExampleItem = Items.LastOrDefault();
}
public ObservableCollection<Item> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
public Item ExampleItem
{
get { return _exampleItem; }
set { _exampleItem = value; OnPropertyChanged();}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
My custom user control is defined like this:
<UserControl x:Class="WpfDataTemplate.ItemRowUserControl"
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="40" d:DesignWidth="300"
x:Name="ItemRowControl" DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}">
<Grid Background="Yellow" Height="40">
<TextBlock Text="{Binding ItemName}" Foreground="Black"/>
</Grid>
</UserControl>
...and it has one DependencyProperty in code behind:
public partial class ItemRowUserControl : UserControl
{
public ItemRowUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty ItemNameProperty = DependencyProperty.Register(
"ItemName", typeof (string), typeof (ItemRowUserControl), new PropertyMetadata(default(string)));
public string ItemName
{
get { return (string) GetValue(ItemNameProperty); }
set { SetValue(ItemNameProperty, value); }
}
}
The problem is, when I try to bind to property of Item in DataTemplate for ItemsControl, which I'm doing in MainWindow like this (note: I have dummy converter for debugging purposes only, returning value back, and nothing more):
<Window.DataContext>
<my:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<my:MyDummyConverter x:Key="MyDummyConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<ItemsControl Name="ItemsControl" ItemsSource="{Binding Items}" Grid.Row="0" Background="Red">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type my:Item}">
<my:ItemRowUserControl ItemName="{Binding ItemNameToBeSureWhatPropertyIsBound, Converter={StaticResource MyDummyConverter}}" />
<!--<Grid Background="Pink">
<TextBlock Text="{Binding ItemNameToBeSureWhatPropertyIsBound, Converter={StaticResource MyDummyConverter}}" Foreground="Black" Height="30" />
</Grid>-->
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid Grid.Row="1">
<my:ItemRowUserControl ItemName="{Binding DataContext.ExampleItem.ItemNameToBeSureWhatPropertyIsBound, ElementName=MyWindow, Converter={StaticResource MyDummyConverter}}" />
</Grid>
</Grid>
Now, in case I bind to my custom ItemRowUserControl, the value I get into converter (and I see the same in Debug Output) is ItemRowUserControl itself. But if I bind to commented out code, everything works fine. Why is that, and how can I have custom control for DataTemplate so that bindings (offered by intellisense) will work? On the side note: binding to my ItemRowUserControl in grid row 1 (at the bottom) works fine, so I guess control is set to work as expected?
The problem is that you explicitly set the DataContext of your UserControl to itself:
DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}
Remove that assignment and write the ItemName binding like this:
<TextBlock Text="{Binding ItemName,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
or like this
<TextBlock Text="{Binding ItemName, ElementName=ItemRowControl}"/>
I'm using MVVM in a windows phone 8 application. I would like to move from 1 view model to another inside my shell view model. I can't seem to get the ContentControl to bind to a template that is a usercontrol/phoneApplicationPage over the view model.
What am I missing?
I'm trying to avoid things like MVVM light. (I want my app to be as small a download as possible) And this should be possible to do.
P.S. I'm still pretty new to WPF/WP8
Here is a sample of what I have so far, Excuse the dumb functionality :)
/** The Shell view **/
<phone:PhoneApplicationPage
x:Class="PhoneAppWithDataContext.Navigation.ViewModelNavigation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
mc:Ignorable="d"
shell:SystemTray.IsVisible="True"
xmlns:vm="clr-namespace:PhoneAppWithDataContext.Navigation">
<phone:PhoneApplicationPage.DataContext>
<vm:AppViewModel/>
</phone:PhoneApplicationPage.DataContext>
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="MonthViewModel">
<vm:MonthViewControl/>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title-->
<StackPanel Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ContentControl Content="{Binding CurrentViewModel}"
ContentTemplate="{Binding ContentTemplate}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
</Grid>
<Button Content="Change VM" Command="{Binding ChangeViewModelCommand}"/>
</Grid>
</phone:PhoneApplicationPage>
/** Shell/Application View Model **/
public class AppViewModel : ViewModelBase
{
private ViewModelBase _currentViewModel;
private List<ViewModelBase> _viewModels = new List<ViewModelBase>();
private byte _month = 1;
public ViewModelBase CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
if (_currentViewModel == value)
return;
_currentViewModel = value;
NotifyPropertyChanged("CurrentViewModel");
}
}
public DataTemplate SelectedTemplate
{
get
{
if (_currentViewModel == null)
return null;
return DataTemplateSelector.GetTemplate(_currentViewModel);
}
}
public List<ViewModelBase> ViewModels
{
get
{
return _viewModels;
}
}
public AppViewModel()
{
ViewModels.Add(new MonthViewModel(_month));
CurrentViewModel = ViewModels.FirstOrDefault();
}
private ICommand _changeViewModelCommand;
public ICommand ChangeViewModelCommand
{
get
{
return _changeViewModelCommand ?? (_changeViewModelCommand = new GenericCommand(() =>
{
_month++;
var newVM = new MonthViewModel(_month);
ViewModels.Add(newVM);
CurrentViewModel = newVM;
}, true));
}
}
private void ChangeViewModel(ViewModelBase viewModel)
{
if (!ViewModels.Contains(viewModel))
ViewModels.Add(viewModel);
CurrentViewModel = ViewModels.FirstOrDefault(vm => vm == viewModel);
}
}
/** DataTemplateSelector **/
public static class DataTemplateSelector
{
public static DataTemplate GetTemplate(ViewModelBase param)
{
Type t = param.GetType();
return App.Current.Resources[t.Name] as DataTemplate;
}
}
/** User Control **/
<UserControl x:Class="PhoneAppWithDataContext.Navigation.MonthViewControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
d:DesignHeight="480" d:DesignWidth="480"
xmlns:vm="clr-namespace:PhoneAppWithDataContext.Navigation">
<UserControl.DataContext>
<vm:MonthViewModel/>
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Id" Width="100" VerticalAlignment="Center" Grid.Row="0" Grid.Column="0" />
<TextBlock Text="{Binding Id}" Width="100" VerticalAlignment="Center" Grid.Row="0" Grid.Column="1" />
<TextBlock Text="Name" Width="100" VerticalAlignment="Center" Grid.Row="1" Grid.Column="0" />
<TextBlock Text="{Binding Name}" Width="100" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" />
</Grid>
</UserControl>
/** ViewModelBase **/
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
/** View model that the user control should bind to **/
public sealed class MonthViewModel : ViewModelBase
{
private byte _id;
private string _name;
public MonthViewModel()
{
}
public MonthViewModel(byte id)
{
_id = id;
_name = "Month " + id.ToString() + " of the year";
}
public override string ToString()
{
return _name;
}
public byte Id
{
get
{
return _id;
}
}
public string Name
{
get
{
return _name;
}
}
}
I believe the problem here is:
<UserControl.DataContext>
<vm:MonthViewModel/>
</UserControl.DataContext>
When your Content is changed from one MonthViewModel to the next, the DataContext of the returned DataTemplate is set to the object bound to Content. Well, once that DataContext is set, you should be good to go, but once the UserControl is loaded, it is resetting the DataContext to a new instace of an empty MonthViewModel (vm:MonthViewModel). Get rid of that explicit DataContext declaration--in other words delete the code that I posted above.
That way, when you first call CurrentViewModel and INPC is raised, it won't reset the DataContext. When you switch between CurrentViewModel's that are of MonthViewModel type, your UserControl won't call InitializeComponent again, instead the DataContext will change.
EDIT
In addition, if you still aren't seeing changes, then I would point to SelectedTemplate property. Instead of the null check in the property, just pass null to the GetTemplate. Inside of GetTemplate, check for null and return null there if it is null.