This user control I am developing was working and now it has quit working and I cannot figure out what changed. (I've closed and re-opened VS2019 several times, it shows no errors when building or running.) For clarity, I've included the pertinent code sections below but I've included all the code at bottom.
MessagePanel.xaml.cs
public string MessageResponse
{
get { return (string)GetValue(MessageResponseProperty); }
set { SetValue(MessageResponseProperty, value); }
}
// Using a DependencyProperty as the backing store for MessageResponse. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MessageResponseProperty =
DependencyProperty.Register("MessageResponse", typeof(string), typeof(MessagePanel), new PropertyMetadata(""));
private void ButtonProceed_Click(object sender, RoutedEventArgs e)
{
MessageResponse = "Proceed";
}
private void ButtonHalt_Click(object sender, RoutedEventArgs e)
{
MessageResponse = "Halt";
}
MessagePanel.xaml
<TextBox Text="{Binding MessageResponse, RelativeSource={RelativeSource AncestorType=UserControl}}" />
MainWindow.xaml
<uc:MessagePanel MessageResponse="{Binding MainMessageResponse}" />
MainVM.cs
private string mainMessageResponse;
public string MainMessageResponse
{
get => mainMessageResponse;
set
{
mainMessageResponse = value;
NotifyPropertyChanged();
}
}
As far as I can tell, the DependencyProperty MessageResponse in MessagePanel should be propagated to the MainMessageResponse property in the view model MainVM.cs. Certainly, if I insert code in the view model to set the MainMessageResponse value, the NotifyPropertyChanged() fires and the value appears in the bound TextBox in MainWindow.xaml. But when I click on either button of the user control, though the value appears in the bound TextBox in MessagePanel.xaml, the value no longer propagates through to MainMessageResponse.
What am I missing here?
Full code follows (stripped to the bare necessities):
MessagePanel.xaml
<UserControl x:Class="Common.UserControls.MessagePanel"
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="100" d:DesignWidth="400"
Visibility="Visible" >
<Grid>
<Border
MinWidth="50" MinHeight="50"
Background="LightCoral"
BorderBrush="Black"
BorderThickness="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel>
<StackPanel Width="400" Margin="5,5,5,0">
<TextBox x:Name="Title" TextWrapping="Wrap" MinHeight="16" Background="LightPink" HorizontalAlignment="Center" />
<TextBox x:Name="Message" IsReadOnly="True" TextWrapping="Wrap" MinHeight="42" Background="LightPink" HorizontalAlignment="Stretch" />
</StackPanel>
<DockPanel >
<CheckBox x:Name="CheckBoxConfirm" Checked="CheckBoxConfirm_Checked" Unchecked="CheckBoxConfirm_Unchecked" FontStyle="Italic" FontWeight="Bold" HorizontalAlignment="Center" />
</DockPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="ButtonProceed" Click="ButtonProceed_Click" Width="50" Margin="5,5" />
<Button x:Name="ButtonHalt" Click="ButtonHalt_Click" Width="50" Margin="5,5" />
</StackPanel>
<TextBox Visibility="Visible" Name="Response" Text="{Binding MessageResponse, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
</Border>
</Grid>
</UserControl>
MesssagePanel.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace Common.UserControls
{
/// <summary>
/// Interaction logic for MessagePanel.xaml
/// </summary>
public partial class MessagePanel : UserControl
{
public enum MessageType
{
Ok,
OkCancel,
YesNo
}
public MessagePanel()
{
InitializeComponent();
Title.Text = "This is a title";
Message.Text = "This is a test message with title and [Yes] and [No] buttons and requires a confirmation.";
ButtonProceed.Content = "Yes";
ButtonHalt.Content = "No";
CheckBoxConfirm.Visibility = Visibility.Collapsed;
}
#region Dependeny Properties
public string MessageResponse
{
get { return (string)GetValue(MessageResponseProperty); }
set { SetValue(MessageResponseProperty, value); }
}
// Using a DependencyProperty as the backing store for MessageResponse. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MessageResponseProperty =
DependencyProperty.Register("MessageResponse", typeof(string), typeof(MessagePanel), new PropertyMetadata(""));
#endregion
#region Event Handlers
private void ButtonProceed_Click(object sender, RoutedEventArgs e)
{
// User wants to proceed
MessageResponse = "Proceed";
}
private void ButtonHalt_Click(object sender, RoutedEventArgs e)
{
// User wants to not proceed
MessageResponse = "Halt";
}
private void CheckBoxConfirm_Checked(object sender, RoutedEventArgs e)
{
ButtonProceed.IsEnabled = true;
}
private void CheckBoxConfirm_Unchecked(object sender, RoutedEventArgs e)
{
ButtonProceed.IsEnabled = false;
}
#endregion
}
}
MainWindow.xaml
<Window x:Class="WpfUserControl.Views.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:uc="clr-namespace:Common.UserControls;assembly=Common"
xmlns:local="clr-namespace:WpfUserControl"
mc:Ignorable="d"
Title="Demo"
Height="300" Width="600">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBox Text="{Binding MainMessageResponse}" Width="50" Height="22" />
</StackPanel>
</Grid>
<uc:MessagePanel MessageResponse="{Binding MainMessageResponse}" />
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using WpfUserControl.ViewModels;
namespace WpfUserControl.Views
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainVM vm = new MainVM();
DataContext = vm;
}
}
}
MainVM.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfUserControl.ViewModels
{
public partial class MainVM : INotifyPropertyChanged
{
public MainVM()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#region MessagePanel
private string mainMessageResponse;
public string MainMessageResponse
{
get => mainMessageResponse;
set
{
mainMessageResponse = value;
NotifyPropertyChanged();
}
}
#endregion
}
}
You should set the Mode of the Binding to TwoWay. You can do this for an individual binding in the XAML markup:
<uc:MessagePanel MessageResponse="{Binding MainMessageResponse, Mode=TwoWay}" />
Or you can specify the default value for all bindings when you register the dependency property in the control:
public static readonly DependencyProperty MessageResponseProperty =
DependencyProperty.Register("MessageResponse", typeof(string), typeof(MessagePanel),
new FrameworkPropertyMetadata("") { BindsTwoWayByDefault = true });
Related
I am working on a project that has one Main Window and two User Controls (Views).
The Main Window has a Menu on the left hand side with some buttons that activate one View or the other. On the right hand side of Main Window there's a Content Control that displays the CurrentView.
My idea was to have a Form for adding people on one View and a DataGrid displaying those people on the other. For that I made an ObservableColection that has People and bound that collection to the DataGrid on the corresponding View.
Everything is working as intended (with the Form I can add people to the collection and then those people get added to the DataGrid) except that when I add a person and then change the View to see if it got added to the DataGrid, nothing is added to the DataGrid but the headers.
If I put the same DataGrid with the same Binding in the first View (where the Form is) I can see that the people are getting added so I think the problem is that when I change Views I am losing the information about the collection.
``
This is the code I'm using to make the navigation between views available.
MainWindow
<Window x:Class="SwitchViewsDemo2.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:vm="clr-namespace:SwitchViewsDemo2.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<vm:NavigationViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<Button Command="{Binding GoToAddPeopleCommand}" Content="Go To Add People" />
<Button Command="{Binding GoToDisplayPeopleCommand}" Content="Go To Display People" />
</StackPanel>
<ContentControl Grid.Column="1" Content="{Binding CurrentView}" />
</Grid>
</Window>
AddPeopleView
<UserControl x:Class="SwitchViewsDemo2.Views.AddPeopleView"
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:vm="clr-namespace:SwitchViewsDemo2.ViewModels"
mc:Ignorable="d" FontSize="28"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<vm:AddPeopleViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Add People" />
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="First Name" />
<TextBox Text="{Binding FirstName}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="1">
<TextBlock Text="Last Name" />
<TextBox Text="{Binding LastName}"/>
</StackPanel>
</Grid>
<StackPanel Grid.Row="1">
<Button Content="Add Person" Command="{Binding AddPersonCommand}"
Height="40" Width="200" VerticalAlignment="Top"/>
<DataGrid ItemsSource="{Binding People}"/>
</StackPanel>
</Grid>
</UserControl>
DisplayPeopleView
<UserControl x:Class="SwitchViewsDemo2.Views.DisplayPeopleView"
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:vm="clr-namespace:SwitchViewsDemo2.ViewModels"
mc:Ignorable="d" FontSize="28"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<vm:DisplayPeopleViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel>
<TextBlock Text="Display People"/>
<DataGrid ItemsSource="{Binding People}"/>
</StackPanel>
</Grid>
</UserControl>
NavigationViewModel
using SwitchViewsDemo2.Utilities;
using System.Windows.Input;
namespace SwitchViewsDemo2.ViewModels
{
public class NavigationViewModel : Utilities.ViewModelBase
{
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set { _currentView = value; OnPropertyChanged(); }
}
public ICommand GoToAddPeopleCommand { get; set; }
public ICommand GoToDisplayPeopleCommand { get; set; }
private void AddPeople(object obj) => CurrentView = new AddPeopleViewModel();
private void DisplayPeople(object obj) => CurrentView = new DisplayPeopleViewModel();
public NavigationViewModel()
{
GoToAddPeopleCommand = new RelayCommand(AddPeople);
GoToDisplayPeopleCommand = new RelayCommand(DisplayPeople);
// Startup View
CurrentView = new AddPeopleViewModel();
}
}
}
ViewModelBase
using SwitchViewsDemo2.Models;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace SwitchViewsDemo2.Utilities
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
private ObservableCollection<PersonModel> _people = new();
public ObservableCollection<PersonModel> People
{
get { return _people; }
set { _people = value; }
}
}
}
RelayCommand
using System;
using System.Windows.Input;
namespace SwitchViewsDemo2.Utilities
{
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler? CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object? parameter) => _execute(parameter);
}
}
DataTemplate(Resource Dictionary)
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:SwitchViewsDemo2.ViewModels"
xmlns:view="clr-namespace:SwitchViewsDemo2.Views">
<DataTemplate DataType="{x:Type vm:AddPeopleViewModel}">
<view:AddPeopleView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:DisplayPeopleViewModel}">
<view:DisplayPeopleView/>
</DataTemplate>
</ResourceDictionary>
App.xaml
<Application x:Class="SwitchViewsDemo2.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SwitchViewsDemo2"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Utilities/DataTemplate.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Edit:
This is how I add the data to the People object
using SwitchViewsDemo2.Models;
using SwitchViewsDemo2.Utilities;
namespace SwitchViewsDemo2.ViewModels
{
public class AddPeopleViewModel : ViewModelBase
{
private string _firstName;
public string FirstName
{
get { return _firstName; }
set { _firstName = value; OnPropertyChanged(); }
}
private string _lastName;
public string LastName
{
get { return _lastName; }
set { _lastName = value; OnPropertyChanged(); }
}
public ButtonCommand AddPersonCommand { get; set; }
public void AddPerson()
{
PersonModel p = new PersonModel()
{
FirstName = FirstName,
LastName = LastName
};
FirstName = string.Empty;
LastName = string.Empty;
People.Add(p);
}
public AddPeopleViewModel()
{
AddPersonCommand = new ButtonCommand(AddPerson);
}
}
}
ButtonCommand:
using System;
using System.Windows.Input;
namespace SwitchViewsDemo2.Utilities
{
public class ButtonCommand : ICommand
{
public event EventHandler? CanExecuteChanged;
private Action _execute;
public ButtonCommand(Action execute)
{
_execute = execute;
}
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
_execute.Invoke();
}
}
}
Remove this:
<UserControl.DataContext>
<vm:AddPeopleViewModel/>
</UserControl.DataContext>
...and this:
<UserControl.DataContext>
<vm:DisplayPeopleViewModel/>
</UserControl.DataContext>
from the XAML markup of the views.
You are creating the AddPeopleViewModel and DisplayPeopleViewModel instances in the NavigationViewModel. You should not create additional instances of the view models in the views.
It's also unclear why're expecting adding an object to a collection in one view model should affect another one.
There should be only one collection of People. Now you have two in two different view models.
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 am creating a program where if you click the button it adds a new label. However the problem is that when you click the add button the label keeps getting stack on top of eachother instead of being a list
Here is the code
c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace firstwpfapp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void addTask(object sender, RoutedEventArgs e)
{
String val = input.ToString();
Label todo = new Label();
todo.Content = val;
List.Children.Add(todo);
}
}
}
xaml
...
<Window x:Class="firstwpfapp.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:firstwpfapp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Margin="-120,-142,0,0" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0*"/>
<ColumnDefinition Width="5*"/>
<ColumnDefinition Width="451*"/>
</Grid.ColumnDefinitions>
<StackPanel x:Name="Wrapper"Background="LightGray" Orientation="Horizontal"></StackPanel>
<TextBox x:Name="input" Grid.Column="2" HorizontalAlignment="Left" Height="31" Margin="106,198,0,0" TextWrapping="Wrap" Text="Enter here" VerticalAlignment="Top" Width="166"/>
<Button Content="Button" Grid.Column="2" HorizontalAlignment="Left" Margin="106,234,0,0" VerticalAlignment="Top" Width="166" Height="26" Click="addTask"/>
<Grid x:Name="List" Grid.Column="2" HorizontalAlignment="Left" Height="391" Margin="507,160,0,0" VerticalAlignment="Top" Width="385"/>
</Grid>
</Window>
the list keeps getting stack on top of another each time the button is pressed
You can go ahead and add your items to a ListView which will stack the items for you as well as include an ItemSource that we can bind to so it will create the rows for each new ToDo item for you:
Note: I have not tested the below; I'm on my Macbook.
Instead of your Wrapper StackLayout, replace it with:
<ListView Name="Wrapper" Grid.Row="0" Grid.ColumnSpan="3" ItemsSource="{Binding Tasks}" />
Now, create a new file called Task.cs which we will use when creating a new type of Task (add the below to the Task.cs file):
public class Task { public string task { get; set;} }
Have your MainWindow inherit from the INotifyPropertyChanged interface INotifyPropertyChanged
public partial class MainWindow : Window, INotifyPropertyChanged
Now update the rest of your code behind of MainWindow to:
private ObservableCollection<Task> _tasks;
//Tasks will store all of the tasks of type Task that we add to it
//as well as be bound to our ListView that will display whatever we add to our Tasks
public ObservableCollection<Task> Tasks
{
get
{
return _tasks;
}
set
{
if (value != _tasks)
{
_tasks = value;
OnPropertyChanged("Tasks");
}
}
}
//Here we implement OnPropertyChanged so our ObservableCollection can be notified
//whenever we have a new task added to or removed from Tasks (this is created when we implement INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
//Create a new Task and add it to our Tasks any time the addTask button is clicked
private void addTask(object sender, RoutedEventArgs e)
{
Tasks.Add(new Task(input.Text));
}
Replace the Grid
<Grid x:Name="List" Grid.Column="2" HorizontalAlignment="Left" Height="391" Margin="507,160,0,0" VerticalAlignment="Top" Width="385"/>
with a StackPanel:
<StackPanel x:Name="List" Grid.Column="2" HorizontalAlignment="Left" Height="391" Margin="507,160,0,0" VerticalAlignment="Top" Width="385"/>
Adding child elements to a grid stacks them on top of each other (as you have noticed)
A StackPanel adds new child elements below (or after) the previous child element.
Try this ...................
XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Center"
Orientation="Horizontal"
Margin="0,50,0,0">
<TextBox Name="Input"
Width="300"
VerticalAlignment="Center"
HorizontalAlignment="Left" />
<Button Name="AddBtn"
Content="Add"
Margin="20,0,0,0"
VerticalAlignment="Center"
Width="100"
Click="AddBtn_Click"/>
</StackPanel>
<ListView Name="ItemListView"
ItemsSource="{Binding Path=LabelItems, Mode=OneWay}"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="20"/>
</Grid>
C# code:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private ObservableCollection<string> _labels = new ObservableCollection<string>();
public ObservableCollection<string> LabelItems
{
get { return _labels; }
set { _labels = value; RaisePropertyChanged(); }
}
private void AddBtn_Click(object sender, RoutedEventArgs e)
{
if(Input.Text != "" && Input.Text != null)
{
LabelItems.Add(Input.Text);
}
}
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged([CallerMemberName]string name = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
I have Operation class and Result class that I use to build a list, I use xaml to bind to the properties and a converter to return some content based on certain properties, Most of this works....
To make it easy I will post the xaml then note what works and what I need help with.
<UserControl x:Class="OperationListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:telerik="http://schemas.telerik.com/2008/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"
xmlns:prism="http://www.codeplex.com/prism"
xmlns:inf="clr-namespace:***;assembly=***"
xmlns:c="clr-namespace:****.***"
d:DesignHeight="300" d:DesignWidth="300">
<Control.Resources>
<c:LanguageTextConverter x:Key="langConverter" />
<c:ResultViewConverter x:Key="statusConverter" />
<c:OpDetailViewConverter x:Key="opConverter" />
</Control.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<telerik:RadGridView Name="OperationGrid"
Grid.Column="0"
RowHeight="75"
SelectionMode="Single"
SelectedItem="{Binding SelectedOperation, Mode=TwoWay}"
ItemsSource="{Binding Operations}"
IsReadOnly="True"
AutoGenerateColumns="False"
RowDetailsVisibilityMode="VisibleWhenSelected"
ShowGroupPanel="False"
RowIndicatorVisibility="Collapsed" >
<telerik:RadGridView.RowDetailsTemplate>
<DataTemplate>
<ContentControl Grid.Column="1" Template="{Binding Converter={StaticResource opConverter}}" />
</DataTemplate>
</telerik:RadGridView.RowDetailsTemplate>
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn Header="Name"
DataMemberBinding="{Binding Key.operationName, Converter={StaticResource langConverter}}"
Width="2*"
IsGroupable="False" />
<telerik:GridViewDataColumn Header="Result"
Width="1*"
MaxWidth="75">
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<Grid>
<ContentControl Template="{Binding Value, Converter={StaticResource statusConverter}}"/>
</Grid>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
</telerik:GridViewDataColumn>
</telerik:RadGridView.Columns>
</telerik:RadGridView>
</Grid>
everything up to the final binding works, and even the final binding works to a point...
<ContentControl Template="{Binding Value, Converter={StaticResource statusConverter}}"/>
this works, when binding to Value the converter fires on load and loads the appropriate xaml.
However no additional changes update this value...
In the code behind at some point the Value.status is updated , the setter on the status property fires an propertyChanged. but nothing is caught on the front end.
public OverallStatus status
{
get { return this.Status; }
set { this.Status = value; onPropertyChanged(this, "status"); }
}
What is the property syntax to bind to a Value.Property (currently doesn't work at all) AND have it recognize the propertyChanged setter 3 levels down from the view.
I have created a sample showing nested properties. When I change a property value at button click, it is reflected in the grid. More can be read here :
Property path syntax
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="524.627" Width="862.313">
<StackPanel>
<DataGrid x:Name="Dgrd" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Value.Status.Name}" ClipboardContentBinding="{x:Null}" Header="Status"/>
</DataGrid.Columns>
</DataGrid>
<Button Content="Button" HorizontalAlignment="Left" Margin="29,38,0,0" Grid.Row="1" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
</StackPanel>
</Window>
using System;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Dgrd.ItemsSource = DataStore.Operations;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
DataStore.Operations[0].Value.Status.Name = "N/A"; // change first item
}
}
public class DataStore
{
//static List<Operation> _operations;
public static List<Operation> Operations;
static DataStore()
{
Operations =
new List<Operation>() {
new Operation(){ Value = new RecordInner(){ Status = new OverallStatus (){ Name="Pending"}}},
new Operation(){ Value = new RecordInner(){ Status = new OverallStatus (){ Name="Started"}}},
new Operation(){ Value = new RecordInner(){ Status = new OverallStatus (){ Name="Completed"}}}
};
}
}
public class Operation
{
RecordInner _value;
public RecordInner Value
{
get { return _value; }
set { _value = value; }
}
}
public class RecordInner
{
OverallStatus _status;
public OverallStatus Status
{
get { return _status; }
set { _status = value; }
}
}
public class OverallStatus:INotifyPropertyChanged
{
string _name;
public string Name { get { return _name; } set { _name = value; OnPropertyChanged("name"); } }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
Hei,
I'm creating simple application with MVVM and stumbled on a problem which i find hard to resolve. On my application i have datagrid and couple of controls to edit currently selected item in datagrid. In my ViewModel i have CurrentSequence property what holds ColorSettingsSequencesSequence object (collection of these objects are used as DataContext for datagrid).
Here's xaml:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Path=ColorSettingsSequences}"
SelectedItem="{Binding Path=CurrentSequence, Mode=TwoWay}">
.... more things here ...
</DataGrid>
<StackPanel Grid.Column="0" Grid.Row="0">
<Grid>
<Label Content="Start temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqStartTemp" VerticalAlignment="Top" />
<TextBox Height="23" Margin="0,28,10,0" x:Name="tbSeqStartTemp" VerticalAlignment="Top" Text="{Binding Path=CurrentSequence.StartTemp}" />
</Grid>
<Grid>
<Label Content="Start color" Height="28" HorizontalAlignment="Left" x:Name="lblSeqHue" VerticalAlignment="Top" />
<xctk:ColorPicker Margin="0,28,10,0" x:Name="clrpSeqHue" SelectedColor="{Binding Path=CurrentSequence.StartHue, Converter={StaticResource hueToColor}, ConverterParameter=False}" ShowStandardColors="False" />
</Grid>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0">
<Grid>
<Label Content="End temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqEndTemp" VerticalAlignment="Top" />
<TextBox Height="23" Margin="0,28,10,0" x:Name="tbSeqEndTemp" VerticalAlignment="Top" Text="{Binding Path=CurrentSequence.EndTemp}" />
</Grid>
<Grid>
<Label Content="End color" Height="28" HorizontalAlignment="Left" x:Name="lblSeqEndHue" VerticalAlignment="Top" />
<xctk:ColorPicker Margin="0,28,10,0" x:Name="clrpSeqEndHue" SelectedColor="{Binding Path=CurrentSequence.EndHue, Converter={StaticResource hueToColor}, ConverterParameter=False}" ShowStandardColors="False" />
</Grid>
</StackPanel>
Code:
private ColorSettingsSequencesSequence _currentSequence;
public ColorSettingsSequencesSequence CurrentSequence
{
get
{
return this._currentSequence;
}
set
{
this._currentSequence = value;
OnPropertyChanged("CurrentSequence");
}
}
That works nicely, but the problem comes when i want to add validation. I would like to validate StartTemp and EndTemp separately and give different errors. How would i break up the ColorSettingsSequencesSequence object so that the bindings would also still work eq if i edit one value it gets updated in the datagrid also?
Here's what i tried, i created 2 new properties and added my validation to those:
private String _currentSequenceStartTemp;
public String CurrentSequenceStartTemp
{
get
{
return _currentSequenceStartTemp;
}
set
{
this._currentSequenceStartTemp = value;
CurrentSequence.StartTemp = value;
RaisePropertyChanged("CurrentSequenceStartTemp");
Validator.Validate(() => CurrentSequenceStartTemp);
ValidateCommand.Execute(null);
}
}
private String _currentSequenceEndTemp;
public String CurrentSequenceEndTemp
{
get
{
return _currentSequenceEndTemp;
}
set
{
this._currentSequenceEndTemp = value;
CurrentSequence.EndTemp = value;
RaisePropertyChanged("CurrentSequenceEndTemp");
Validator.Validate(() => CurrentSequenceEndTemp);
ValidateCommand.Execute(null);
}
}
And the i just binded TextBoxes to those values, instead of binding them straight to CurrentSequence. I also added setting the CurrentSequence values in the setters and hoped that way my changes will be pushed all the way back to original collection and will be changed in datagrid. That didn't happen..
When CurrentSequence is changed i change values of these properties also:
private ColorSettingsSequencesSequence _currentSequence;
public ColorSettingsSequencesSequence CurrentSequence
{
get
{
return this._currentSequence;
}
set
{
this._currentSequence = value;
RaisePropertyChanged("CurrentSequence");
if (value != null)
{
CurrentSequenceStartTemp = value.StartTemp;
CurrentSequenceEndTemp = value.EndTemp;
}
else
{
CurrentSequenceStartTemp = String.Empty;
CurrentSequenceEndTemp = String.Empty;
}
}
}
I have reproduced your problem. But I couldn't find any problem. Everything works fine.
Validate StartTemp and EndTemp separately.
If one value is updated, the datagrid should also be updated
So I have solved above two problems in my project.
The Results
After changing start temperature to 40, the datagrid value also has been changed.
Let's create an error in start temperature text box.
And now the other one
You can see now both the properties are validated separately.
This is the project I have created.
Project Structure
ViewModelBase class
public class ViewModelBase : INotifyPropertyChanged
{
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, args);
}
#endregion
}
ColorSettingsSequencesSequence class
public class ColorSettingsSequencesSequence : ViewModelBase, IDataErrorInfo
{
#region Declarations
private string startColor;
private string startTemperature;
private string endTemperature;
#endregion
#region Properties
/// <summary>
/// Gets or sets the start color.
/// </summary>
/// <value>
/// The start color.
/// </value>
public string StartColor
{
get
{
return this.startColor;
}
set
{
this.startColor = value;
OnPropertyChanged("StartColor");
}
}
/// <summary>
/// Gets or sets the start temperature.
/// </summary>
/// <value>
/// The start temperature.
/// </value>
public string StartTemperature
{
get
{
return this.startTemperature;
}
set
{
this.startTemperature = value;
OnPropertyChanged("StartTemperature");
}
}
/// <summary>
/// Gets or sets the end temperature.
/// </summary>
/// <value>
/// The end temperature.
/// </value>
public string EndTemperature
{
get
{
return this.endTemperature;
}
set
{
this.endTemperature = value;
OnPropertyChanged("EndTemperature");
}
}
#endregion
/// <summary>
/// Gets an error message indicating what is wrong with this object.
/// </summary>
/// <returns>An error message indicating what is wrong with this object. The default is an empty string ("").</returns>
public string Error
{
get
{
return "";
}
}
/// <summary>
/// Gets the error message for the property with the given name.
/// </summary>
/// <param name="columnName">Name of the column.</param>
/// <returns></returns>
public string this[string columnName]
{
get
{
if (columnName.Equals("StartTemperature"))
{
if (string.IsNullOrEmpty(this.StartTemperature))
{
return "Please enter a start temperature";
}
}
if (columnName.Equals("EndTemperature"))
{
if (string.IsNullOrEmpty(this.EndTemperature))
{
return "Please enter a end temperature";
}
}
return "";
}
}
}
MainViewModel
public class MainViewModel : ViewModelBase
{
#region Declarations
private ColorSettingsSequencesSequence currentSequence;
private ObservableCollection<ColorSettingsSequencesSequence> colorSettingsSequences;
#endregion
#region Properties
/// <summary>
/// Gets or sets the current sequence.
/// </summary>
/// <value>
/// The current sequence.
/// </value>
public ColorSettingsSequencesSequence CurrentSequence
{
get
{
return this.currentSequence;
}
set
{
this.currentSequence = value;
OnPropertyChanged("CurrentSequence");
}
}
/// <summary>
/// Gets or sets the color settings sequences.
/// </summary>
/// <value>
/// The color settings sequences.
/// </value>
public ObservableCollection<ColorSettingsSequencesSequence> ColorSettingsSequences
{
get
{
return this.colorSettingsSequences;
}
set
{
this.colorSettingsSequences = value;
OnPropertyChanged("ColorSettingsSequences");
}
}
#endregion
#region Commands
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="MainViewModel" /> class.
/// </summary>
public MainViewModel()
{
this.ColorSettingsSequences = new ObservableCollection<ColorSettingsSequencesSequence>();
ColorSettingsSequencesSequence sequence1 = new ColorSettingsSequencesSequence();
sequence1.StartColor = "Blue";
sequence1.StartTemperature = "10";
sequence1.EndTemperature = "50";
ColorSettingsSequences.Add(sequence1);
ColorSettingsSequencesSequence sequence2 = new ColorSettingsSequencesSequence();
sequence2.StartColor = "Red";
sequence2.StartTemperature = "20";
sequence2.EndTemperature = "60";
ColorSettingsSequences.Add(sequence2);
ColorSettingsSequencesSequence sequence3 = new ColorSettingsSequencesSequence();
sequence3.StartColor = "Yellow";
sequence3.StartTemperature = "30";
sequence3.EndTemperature = "70";
ColorSettingsSequences.Add(sequence3);
this.CurrentSequence = sequence1;
}
#endregion
#region Private Methods
#endregion
}
MainWindow.xaml (XAML)
<Window x:Class="DataGridValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Name="mainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="149" />
<RowDefinition Height="73" />
<RowDefinition Height="123" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="249*" />
</Grid.ColumnDefinitions>
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding ColorSettingsSequences}"
SelectedItem="{Binding CurrentSequence}"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Start Color" Binding="{Binding StartColor}" />
<DataGridTextColumn Header="End Color" Binding="{Binding StartTemperature}" />
<DataGridTextColumn Header="End Color" Binding="{Binding EndTemperature}" />
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Column="0" Grid.Row="1">
<Grid>
<Label Content="Start temperature (°C)"
Height="28"
HorizontalAlignment="Left"
x:Name="lblSeqStartTemp"
VerticalAlignment="Top" />
<TextBox Height="23"
Margin="10,28,10,0"
x:Name="tbSeqStartTemp"
VerticalAlignment="Top"
Text="{Binding Path=CurrentSequence.StartTemperature, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>
</Grid>
</StackPanel>
<StackPanel Grid.Row="2" Margin="0,0,0,43">
<Grid>
<Label Content="End temperature (°C)"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
<TextBox Height="23"
Margin="10,28,10,0"
x:Name="tbSeqEndTemp"
VerticalAlignment="Top"
Text="{Binding Path=CurrentSequence.EndTemperature, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>
</Grid>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs (Code behind file)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
mainGrid.DataContext = new MainViewModel();
}
}
If I have understood correctly, your problem is that you want to commit your property value even if the validation fails. In case I am wrong in this assumption, the solution is even easier, basically what sine hinted at in his comment, that you would only need to implement INotifyPropertyChanged in your ColorSettingsSequencesSequence class.
I couldn't infer from your post what kind of validation you employ, but here is how I'd do it. The key to updating your datagrid even if validation in the textbox fails, is the ValidationStep="UpdatedValue" part of the ValidationRule (and the implementation of the rule).
View:
<UserControl x:Class="WpfApplication1.DemoValidation"
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:WpfApplication1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<local:DemoValidationViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder Name="ph" />
</Border>
<Border BorderBrush="LightGray" BorderThickness="1" Background="Beige">
<TextBlock Foreground="Red" FontSize="12" Margin="5"
Text="{Binding ElementName=ph, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
</TextBlock>
</Border>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Grid.Row="0">
<Label Content="Start temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqStartTemp" VerticalAlignment="Top" />
<TextBox Height="23" x:Name="tbSeqStartTemp" VerticalAlignment="Top" >
<TextBox.Text>
<Binding Path="CurrentSequence.StartTemp" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:TempValidationRule MaximumTemp="400" MinimumTemp="-100" ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Column="2" Grid.Row="0">
<Label Content="End temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqEndTemp" VerticalAlignment="Top" />
<TextBox Height="23" x:Name="tbSeqEndTemp" VerticalAlignment="Top" >
<TextBox.Text>
<Binding Path="CurrentSequence.EndTemp" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<local:TempValidationRule MaximumTemp="500" MinimumTemp="100" ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
<DataGrid Grid.Row="2" Grid.ColumnSpan="3" Margin="0,10,0,0"
ItemsSource="{Binding Path=ColorSettingsSequences}"
SelectedItem="{Binding Path=CurrentSequence, Mode=TwoWay}" />
</Grid>
</UserControl>
ViewModel:
public class DemoValidationViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private ColorSettingsSequencesSequence _currentSequence;
public ColorSettingsSequencesSequence CurrentSequence
{
get { return this._currentSequence; }
set
{
this._currentSequence = value;
OnPropertyChanged("CurrentSequence");
}
}
public List<ColorSettingsSequencesSequence> ColorSettingsSequences { get; private set; }
public DemoValidationViewModel()
{
// dummy data
this.ColorSettingsSequences = new List<ColorSettingsSequencesSequence>()
{
new ColorSettingsSequencesSequence() { StartTemp = "10", EndTemp = "20" },
new ColorSettingsSequencesSequence() { StartTemp = "20", EndTemp = "30" },
new ColorSettingsSequencesSequence() { StartTemp = "30", EndTemp = "40" }
};
}
}
ColorSettingsSequencesSequence:
public class ColorSettingsSequencesSequence : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string _startTemp;
public string StartTemp { get { return _startTemp; } set { _startTemp = value; OnPropertyChanged("StartTemp");}}
private string _endTemp;
public string EndTemp { get { return _endTemp; } set { _endTemp = value; OnPropertyChanged("EndTemp"); } }
}
ValidationRule (see also this thread):
public class TempValidationRule : ValidationRule
{
// default values
private int _minimumTemp = -273;
private int _maximumTemp = 2500;
public int MinimumTemp
{
get { return _minimumTemp; }
set { _minimumTemp = value; }
}
public int MaximumTemp
{
get { return _maximumTemp; }
set { _maximumTemp = value; }
}
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string error = null;
string s = GetBoundValue(value) as string;
if (!string.IsNullOrEmpty(s))
{
int temp;
if (!int.TryParse(s, out temp))
error = "No valid integer";
else if (temp > this.MaximumTemp)
error = string.Format("Temperature too high. Maximum is {0}.", this.MaximumTemp);
else if (temp < this.MinimumTemp)
error = string.Format("Temperature too low. Minimum is {0}.", this.MinimumTemp);
}
return new ValidationResult(string.IsNullOrEmpty(error), error);
}
private object GetBoundValue(object value)
{
if (value is BindingExpression)
{
// ValidationStep was UpdatedValue or CommittedValue (validate after setting)
// Need to pull the value out of the BindingExpression.
BindingExpression binding = (BindingExpression)value;
// Get the bound object and name of the property
string resolvedPropertyName = binding.GetType().GetProperty("ResolvedSourcePropertyName", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null).ToString();
object resolvedSource = binding.GetType().GetProperty("ResolvedSource", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null);
// Extract the value of the property
object propertyValue = resolvedSource.GetType().GetProperty(resolvedPropertyName).GetValue(resolvedSource, null);
return propertyValue;
}
else
{
return value;
}
}
}