Binding doesn't work in ListView - c#

I have a wpf control named DataPicker which has a dependency property named SelectedDate.
In simple cases it works well but there is one case where binding fails and I can't understand why: when i try to bind it inside a ListView.
For example, I have class (INotifyPropertyChanged is implemented)
public class TestClass : INotifyPropertyChanged
{
public string Name { get; set; }
public DateTime Date { get; set; }
}
and try to bind sample collection like
public ObservableCollection<TestClass> Items { get; set; }
which has one element in it.
Binding looks like
<Window x:Class="Neverov.Test.Window1"
x:Name="this"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Neverov.Framework;assembly=Neverov.Framework">
<Grid>
<ListView ItemsSource="{Binding ElementName=this, Path=Items}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<local:DatePicker SelectedDate="{Binding Date, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
and Name property works fine.
inside my DatePicker date value is shown this way:
<TextBox x:Name="PART_TextBox">
<TextBox.Text>
<Binding Path="SelectedDate"
RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:DatePicker}}"
Mode="TwoWay"
Converter="{StaticResource DateTimeConverter}"
ConverterParameter="d">
</Binding>
</TextBox.Text>
</TextBox>
any ideas why this could happen?
More code of the DatePicker class: (some specific properties that I need I'll rather miss to keep code size not so large)
[TemplatePart(Name = PartPopup, Type = typeof(Popup))]
[TemplatePart(Name = PartMonthBack, Type = typeof(ButtonBase))]
[TemplatePart(Name = PartMonthForward, Type = typeof(ButtonBase))]
[TemplatePart(Name = PartDates, Type = typeof(Selector))]
[TemplatePart(Name = PartTextBox, Type = typeof(TextBox))]
[TemplatePart(Name = PartCheckBox, Type = typeof(CheckBox))]
[TemplatePart(Name = PartToggleButton, Type = typeof(ToggleButton))]
public class DatePicker : Control, INotifyPropertyChanged
{
...
public static readonly DependencyProperty SelectedDateProperty =
DependencyProperty.Register("SelectedDate",
typeof(DateTime?),
typeof(DatePicker),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
(sender, e) =>
{
var datePicker = sender as DatePicker;
if (datePicker != null)
{
var oldValue = e.OldValue as DateTime?;
DateTime selectedDateForPopup =
datePicker.SelectedDate ??
DateTime.Now;
datePicker.CurrentlyViewedMonth =
selectedDateForPopup.Month;
datePicker.CurrentlyViewedYear =
selectedDateForPopup.Year;
datePicker.OnDateChanged(datePicker.SelectedDate, oldValue);
var popup = datePicker.GetTemplateChild(PartPopup) as Popup;
if (popup != null)
popup.IsOpen = false;
}
}));
... //a lot more not so important code here
}

Make sure your properties throw the INotifyPropertyChanged event:
public class TestClass : INotifyPropertyChanged
{
private DateTime date;
public DateTime Date
{
get { return date; }
set { date = value; NotifyPropertyChanged("Date"); }
}
private void NotifyPropertyChanged(string info)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
And make your binding to the date also TwoWay:
<local:DatePicker SelectedDate="{Binding Date, Mode=TwoWay}"/>

Check the output window for any data binding errors..
Probable errors:
DataContext not set correctly.
ListView ItemsSource="{Binding ElementName=this, Path=Items} Isn't Items a property of the Window or a ViewModel class ?

The problem is solved.
It was not in wrong bindings or other difficult and uneven stuff, but, as it was legacy code, somewhere in code programmer made a mistake.
Thank for all!

Related

Set DataContext for multiple controls with XAML

I have different groups of controls bound to different categories of ViewModel classes.
The ViewModels are
MainViewModel
VideoViewModel
AudioViewModel
Question
How can I set the DataContext with XAML instead of C#?
1. I tried adding DataContext="{Binding VideoViewModel}" to the ComboBox XAML, but it didn't work and the items came up empty.
2. I also tried grouping all the ComboBoxes of a certain category inside a UserControl with the DataContext:
<UserControl DataContext="{Binding VideoViewModel}">
<!-- ComboBoxes in here -->
</UserControl>
3. Also tried setting the <Window> DataContext to itself DataContext="{Binding RelativeSource={RelativeSource Self}}"
Data Context
I'm currently setting the DataContext this way for the different categories of controls:
public MainWindow()
{
InitializeComponent();
// Main
this.DataContext =
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
// Video
cboVideo_Codec.DataContext =
cboVideo_Quality.DataContext =
tbxVideo_BitRate.DataContext =
cboVideo_Scale.DataContext =
VideoViewModel.vm;
// Audio
cboAudio_Codec.DataContext =
cboAudio_Quality.DataContext =
tbxAudio_BitRate.DataContext =
tbxAudio_Volume.DataContext =
AudioViewModel.vm;
}
XAML ComboBox
<ComboBox x:Name="cboVideo_Quality"
DataContext="{Binding VideoViewModel}"
ItemsSource="{Binding Video_Quality_Items}"
SelectedItem="{Binding Video_Quality_SelectedItem}"
IsEnabled="{Binding Video_Quality_IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="105"
Height="22"
Margin="0,0,0,0"/>
Video ViewModel Class
public class VideoViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void OnPropertyChanged(string prop)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(prop));
}
}
public VideoViewModel() { }
public static VideoViewModel _vm = new VideoViewModel();
public static VideoViewModel vm
{
get { return _vm; }
set
{
_vm = value;
}
}
// Items Source
private List<string> _Video_Quality_Items = new List<string>()
{
"High",
"Medium",
"Low",
};
public List<string> Video_Quality_Items
{
get { return _Video_Quality_Items; }
set
{
_Video_Quality_Items = value;
OnPropertyChanged("Video_Quality_Items");
}
}
// Selected Item
private string _Video_Quality_SelectedItem { get; set; }
public string Video_Quality_SelectedItem
{
get { return _Video_Quality_SelectedItem; }
set
{
if (_Video_Quality_SelectedItem == value)
{
return;
}
_Video_Quality_SelectedItem = value;
OnPropertyChanged("Video_Quality_SelectedItem");
}
}
// Enabled
private bool _Video_Quality_IsEnabled;
public bool Video_Quality_IsEnabled
{
get { return _Video_Quality_IsEnabled; }
set
{
if (_Video_Quality_IsEnabled == value)
{
return;
}
_Video_Quality_IsEnabled = value;
OnPropertyChanged("Video_Quality_IsEnabled");
}
}
}
You can instantiate an object in xaml:
<Window.DataContext>
<local:MainWindowViewmodel/>
</Window.DataContext>
And you could do that for your usercontrol viewmodels as well.
It's more usual for any child viewmodels to be instantiated in the window viewmodel. Exposed as public properties and the datacontext of a child viewmodel then bound to that property.
I suggest you google viewmodel first and take a look at some samples.
I'm not sure if this is the correct way, but I was able to bind groups of ComboBoxes to different ViewModels.
I created one ViewModel to reference them all.
public class VM: INotifyPropertyChanged
{
...
public static MainViewModel MainView { get; set; } = new MainViewModel ();
public static VideoViewModel VideoView { get; set; } = new VideoViewModel ();
public static AudioViewModel AudioView { get; set; } = new AudioViewModel ();
}
I used Andy's suggestion <local:VM> in MainWindow.xaml.
<Window x:Class="MyProgram.MainWindow"
...
xmlns:local="clr-namespace:MyProgram"
>
<Window.DataContext>
<local:VM/>
</Window.DataContext>
And used a UserControl with DataContext set to VideoView, with ComboBoxes inside.
Instead of a UserControl, can also just use VideoView.Your_Property_Name on each binding.
<UserControl DataContext="{Binding VideoView}">
<StackPanel>
<ComboBox x:Name="cboVideo_Quality"
ItemsSource="{Binding Video_Quality_Items}"
SelectedItem="{Binding Video_Quality_SelectedItem}"
IsEnabled="{Binding Video_Quality_IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="105"
Height="22"
Margin="0,0,0,0"/>
<!-- Other ComboBoxes with DataContext VideoView in here -->
</StackPanel>
</UserControl>
Then to access one of the properties:
VM.VideoView.Video_Codec_SelectedItem = "x264";
VM.VideoView.Video_Quality_SelectedItem = "High";
VM.AudioView.Audio_Codec_SelectedItem = "AAC";
VM.AudioView.Audio_Quality_SelectedItem = "320k";
Others have obviously provided good answers, however, the underlying miss of your binding is your first set of DataContext = = = = = to the main view model.
Once you the main form's data context to the MAIN view model, every control there-under is expecting ITS STARTING point as the MAIN view model. Since the MAIN view model does not have a public property UNDER IT of the video and audio view models, it cant find them to bind do.
If you remove the "this.DataContext =", then there would be no default data context and each control SHOULD be able to be bound as you intended them.
So change
this.DataContext =
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
to
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;

WPF Format Binding not working

I want to be able to show a distance in both metric and imperial units.However changing the unit system through a ComboBox isn't changing the format of the label.
Some details:
1) The data context is working fine
2) I get "4.00 km" when I start the program but changing the Combobox's value has no effect.
3) ObservableObject has OnPropertyChanged() and it's also working fine everywhere except here.
WPF UI
<ComboBox ItemsSource="{Binding UnitSystems}" SelectedValue="{Binding Units}"/>
<Label Content="{Binding Distance}" ContentStringFormat="{Binding DistanceFormat}"/>
C# View Model
public class ViewModel : ObservableObject
{
private double distance = 4;
public double Distance
{
get
{
return distance;
}
set
{
distance = value;
OnPropertyChanged("Distance");
}
}
private UnitSystem units;
public List<UnitSystem> UnitSystems
{
get
{
return new List<UnitSystem>((UnitSystem[])Enum.GetValues(typeof(UnitSystem)));
}
}
public UnitSystem Units
{
get
{
return units;
}
set
{
units = value;
OnPropertyChanged("Units");
OnPropertyChanged("DistanceFormat");
OnPropertyChanged("Distance");
}
}
public string DistanceFormat
{
get
{
if (Units == UnitSystem.Metric)
return "0.00 km";
else
return "0.00 mi";
}
}
}
public enum UnitSystem
{
Metric,
Imperial
}
Edit: The multibinding solution below has the same problem and it's not because of the format strings. Using f3 and f4, I start with "4.000" and it doesn't change to "4.0000".
<ComboBox ItemsSource="{Binding UnitSystems}" SelectedValue="{Binding Units}"/>
<Label>
<Label.Content>
<MultiBinding Converter="{StaticResource FormatConverter}">
<Binding Path="Distance"/>
<Binding Path="DistanceFormat"/>
</MultiBinding>
</Label.Content>
</Label>
Edit 2 (SOLVED): The problem was that ObservableObject didn't implement INotifyPropertyChanged and surprisingly worked fine until this point.
Bind Units to SelectedItem instead of SelectedValue:
<ComboBox
ItemsSource="{Binding UnitSystems}"
SelectedItem="{Binding Units}"
/>
SelectedValue is used when you use SelectedValuePath="SomePropertyName" to specify a property of the selected item that you care about. But enums have no properties, so just grab the item itself.
Here's my stand-in for ObservableObject:
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string prop = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
Nothing clever. I create those with a snippet.

IValueConverter - Source and Converter in the same Binding/ Same Control using both the Source and Converter

I have a set of comboboxes and textboxes like this:
C1 T1
C2 T2
C3 T3
I implemented an IValueConverter to set the TimeZone in C1 and get the corresponding time in T1. Same for the other pairs.
What I want to do is : if a user manually changes the time in T1, the time in T2 and T3 must change corresponding to T1 as well as according to the TimeZone.
T1 is not the reference though. If any of the textboxes have their value changed, all other textboxes must change as well.
This change can happen:
If the TimeZone is changed in the Combobox
if the User manually changes the time by typing in the text box
Here is my full code:
public partial class MainWindow : Window
{
public static int num;
public static bool isUserInteraction;
public static DateTime timeAll;
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ReadOnlyCollection<TimeZoneInfo> TimeZones = TimeZoneInfo.GetSystemTimeZones();
this.DataContext = TimeZones;
cmb_TZ1.SelectedIndex = 98;
cmb_TZ2.SelectedIndex = 46;
cmb_TZ3.SelectedIndex = 84;
cmb_TZ4.SelectedIndex = 105;
cmb_TZ5.SelectedIndex = 12;
}
private void ComboBox_Selection(object Sender, SelectionChangedEventArgs e)
{
var cmbBox = Sender as ComboBox;
DateTime currTime = DateTime.UtcNow;
TimeZoneInfo tst = (TimeZoneInfo)cmbBox.SelectedItem;
if (isUserInteraction)
{
/* txt_Ctry1.Text=
txt_Ctry2.Text =
txt_Ctry3.Text =
txt_Ctry4.Text =
txt_Ctry5.Text =*/
isUserInteraction = false;
}
}
private void TextBox_Type(object Sender, TextChangedEventArgs e)
{
var txtBox = Sender as TextBox;
if (isUserInteraction)
{
timeAll = DateTime.Parse(txtBox.Text);
if (txtBox.Name != "txt_Ctry1")
txt_Ctry1.Text=
if (txtBox.Name != "txt_Ctry2")
txt_Ctry2.Text =
if (txtBox.Name != "txt_Ctry3")
txt_Ctry3.Text =
if (txtBox.Name != "txt_Ctry4")
txt_Ctry4.Text =
if (txtBox.Name != "txt_Ctry5")
txt_Ctry5.Text =
isUserInteraction = false;
}
}
private void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
isUserInteraction = true;
}
}
public class TimeZoneConverter : IValueConverter
{
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
if (MainWindow.isUserInteraction == false)
{
return value == null ? string.Empty : TimeZoneInfo
.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, (TimeZoneInfo)value)
.ToString("HH:mm:ss dd MMM yy");
}
else
{
return value == null ? string.Empty : TimeZoneInfo
.ConvertTime(MainWindow.timeAll, TimeZoneInfo.Utc, (TimeZoneInfo)value)
.ToString("HH:mm:ss dd MMM yy");
}
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
XAML:
<Window x:Class="Basic_WorldClock.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:src="clr-namespace:System;assembly=mscorlib"
xmlns:sys="clr-namespace:System;assembly=System.Core"
xmlns:local="clr-namespace:Basic_WorldClock"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<Window.Resources>
<ObjectDataProvider x:Key="timezone" ObjectType="{x:Type
sys:TimeZoneInfo}" MethodName="GetSystemTimeZones">
</ObjectDataProvider>
<local:TimeZoneConverter x:Key="timezoneconverter"/>
</Window.Resources>
<Grid Margin="0,0.909,0,-0.909">
<TextBox x:Name="txt_Time1" Text="{Binding ElementName=cmb_TZ1, Path=SelectedValue, Converter={StaticResource timezoneconverter}}" VerticalAlignment="Top"/>
<TextBox x:Name="txt_Time2" Text="{Binding ElementName=cmb_TZ2, Path=SelectedValue, Converter={StaticResource timezoneconverter}}" VerticalAlignment="Top"/>
<TextBox x:Name="txt_Time3" Text="{Binding ElementName=cmb_TZ3, Path=SelectedValue, Converter={StaticResource timezoneconverter}}" Height="23.637" VerticalAlignment="Bottom"/>
<ComboBox x:Name="cmb_TZ1" SelectionChanged="ComboBox_Selection" PreviewMouseDown="OnPreviewMouseDown" ItemsSource="{Binding Source={StaticResource timezone}}" HorizontalAlignment="Right" Height="22.667" Margin="0,89.091,51.667,0" VerticalAlignment="Top" Width="144.666"/>
<ComboBox x:Name="cmb_TZ2" SelectionChanged="ComboBox_Selection" PreviewMouseDown="OnPreviewMouseDown" ItemsSource="{Binding Source={StaticResource timezone}}" HorizontalAlignment="Right" Height="22.667" Margin="0,131.091,52.667,0" VerticalAlignment="Top" Width="144.666"/>
<ComboBox x:Name="cmb_TZ3" SelectionChanged="ComboBox_Selection" PreviewMouseDown="OnPreviewMouseDown" ItemsSource="{Binding Source={StaticResource timezone}}" HorizontalAlignment="Right" Height="22.667" Margin="0,0,48.334,123.575" VerticalAlignment="Bottom" Width="144.666"/>
</Grid>
Question: How can I cascade the corresponding changes to other text boxes by using the Convert method the same way the combox does?
I can use the TextChanged method to capture changes in the reference text box and I can add a few lines of code to make those changes, but I want to use the IValueConverter. Can I have the Source and Converter in the same binding for a textbox?
Without a good Minimal, Complete, and Verifiable code example that shows clearly what you've got so far, it's difficult to provide precise information. But based on what you've described, it seems the main problem here is that you're not using the normal "view model"-based techniques that WPF is designed to be used with. In addition, you are binding the Text property to the time zone, rather than the time, so what WPF wants to update when the Text property changes is actually the combo box selection rather than the time itself.
The first thing you need to do is, instead of having your controls refer to each other, create a view model class that represents the actual state you want to display, and then use that for your DataContext in the window, binding appropriate properties to the particular controls.
And what are those appropriate properties? Well, based on your description you actually have just four: 1) The actual time, and 2) through 4) the three time zones you want to handle.
So, something like this:
class ViewModel : INotifyPropertyChanged
{
// The actual time. Similar to the "timeAll" field you have in the code now
// Should be kept in UTC
private DateTime _time;
// The three selected TimeZoneInfo values for the combo boxes
private TimeZoneInfo _timeZone1;
private TimeZoneInfo _timeZone2;
private TimeZoneInfo _timeZone3;
public DateTime Time
{
get { return _time; }
set { UpdateValue(ref _time, value); }
}
public TimeZoneInfo TimeZone1
{
get { return _timeZone1; }
set { UpdateValue(ref _timeZone1, value); }
}
public TimeZoneInfo TimeZone2
{
get { return _timeZone2; }
set { UpdateValue(ref _timeZone2, value); }
}
public TimeZoneInfo TimeZone3
{
get { return _timeZone3; }
set { UpdateValue(ref _timeZone3, value); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void UpdateValue<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(field, value))
{
field = value;
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
(People often encapsulate the PropertyChanged event and UpdateValue() method in a base class that can be reused for all your view model types.)
With that, then you write an implementation of IMultiValueConverter that takes as the input the combo box index (i.e. Index1, Index2, or Index3 as appropriate) and the Time property value, uses those two values to produce the time-zone converted value for the text box, which is bound using those two values and the converter.
The converter's Convert() method will do the above conversion. Then you'll need to make the ConvertBack() method use the appropriate combo box value to convert back to the UTC time.
Unfortunately, there's a bit of a wrinkle here. Your converter will not normally have access to that value. The IMultiValueConverter.ConvertBack() method only gets the bound target value, and is expected to convert back to the bound source values from that. It's not designed to allow you to update one source value based on another source value and the target value.
There are a number of ways around this limitation, but none that I know of are very elegant.
One option uses the view model exactly as I've shown above. The trick is that you'll need to pass via ConverterParameter a reference to the ComboBox associated with the bound Text property, so that the ConvertBack() method can use the currently selected value (you can't pass the currently selected value itself as the ConverterParamater value, because ConverterParameter is not a dependency property and so can't be the target of a property binding).
Done this way, you might have a converter that looks like this:
class TimeConverter : IMultiValueConverter
{
public string Format { get; set; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
DateTime utc = (DateTime)values[0];
TimeZoneInfo tzi = (TimeZoneInfo)values[1];
return tzi != null ? TimeZoneInfo.ConvertTime(utc, tzi).ToString(Format) : Binding.DoNothing;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
string timeText = (string)value;
DateTime time;
if (!DateTime.TryParseExact(timeText, Format, null, DateTimeStyles.None, out time))
{
return new object[] { Binding.DoNothing, Binding.DoNothing };
}
ComboBox comboBox = (ComboBox)parameter;
TimeZoneInfo tzi = (TimeZoneInfo)comboBox.SelectedValue;
return new object[] { TimeZoneInfo.ConvertTime(time, tzi, TimeZoneInfo.Utc), Binding.DoNothing };
}
}
And XAML that looks like this:
<Window x:Class="TestSO38517212BindTimeZoneAndTime.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:l="clr-namespace:TestSO38517212BindTimeZoneAndTime"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:ViewModel/>
</Window.DataContext>
<Window.Resources>
<ObjectDataProvider x:Key="timezone"
ObjectType="{x:Type s:TimeZoneInfo}"
MethodName="GetSystemTimeZones">
</ObjectDataProvider>
<l:TimeConverter x:Key="timeConverter" Format="HH:mm:ss dd MMM yy"/>
<p:Style TargetType="ComboBox">
<Setter Property="Width" Value="200"/>
</p:Style>
<p:Style TargetType="TextBox">
<Setter Property="Width" Value="120"/>
</p:Style>
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding Time}"/>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="comboBox1" ItemsSource="{Binding Source={StaticResource timezone}}" SelectedValue="{Binding TimeZone1}"/>
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource timeConverter}"
ConverterParameter="{x:Reference Name=comboBox1}"
UpdateSourceTrigger="PropertyChanged">
<Binding Path="Time"/>
<Binding Path="SelectedValue" ElementName="comboBox1"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="comboBox2" ItemsSource="{Binding Source={StaticResource timezone}}" SelectedValue="{Binding TimeZone2}"/>
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource timeConverter}"
ConverterParameter="{x:Reference Name=comboBox2}"
UpdateSourceTrigger="PropertyChanged">
<Binding Path="Time"/>
<Binding Path="SelectedValue" ElementName="comboBox2"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="comboBox3" ItemsSource="{Binding Source={StaticResource timezone}}" SelectedValue="{Binding TimeZone3}"/>
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource timeConverter}"
ConverterParameter="{x:Reference Name=comboBox3}"
UpdateSourceTrigger="PropertyChanged">
<Binding Path="Time"/>
<Binding Path="SelectedValue" ElementName="comboBox3"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Window>
This will work fine at run-time. But you will get design-time _"Object reference not set to an instance of an object" error messages, due to the use of {x:Reference ...} in the ConverterParameter assignments. To some, this is a minor inconvenience, but I find it a huge annoyance and am willing to go to a fair amount of effort to avoid it. :)
So, here's a completely different approach, which foregoes the converter altogether and puts all of the logic inside the view model itself:
class ViewModel : INotifyPropertyChanged
{
private string _ktimeFormat = "HH:mm:ss dd MMM yy";
// The actual time. Similar to the "timeAll" field you have in the code now
// Should be kept in UTC
private DateTime _time = DateTime.UtcNow;
// The three selected TimeZoneInfo values for the combo boxes
private TimeZoneInfo _timeZone1 = TimeZoneInfo.Utc;
private TimeZoneInfo _timeZone2 = TimeZoneInfo.Utc;
private TimeZoneInfo _timeZone3 = TimeZoneInfo.Utc;
// The text to display for each local time
private string _localTime1;
private string _localTime2;
private string _localTime3;
public ViewModel()
{
_localTime1 = _localTime2 = _localTime3 = _time.ToString(_ktimeFormat);
}
public DateTime Time
{
get { return _time; }
set { UpdateValue(ref _time, value); }
}
public TimeZoneInfo TimeZone1
{
get { return _timeZone1; }
set { UpdateValue(ref _timeZone1, value); }
}
public TimeZoneInfo TimeZone2
{
get { return _timeZone2; }
set { UpdateValue(ref _timeZone2, value); }
}
public TimeZoneInfo TimeZone3
{
get { return _timeZone3; }
set { UpdateValue(ref _timeZone3, value); }
}
public string LocalTime1
{
get { return _localTime1; }
set { UpdateValue(ref _localTime1, value); }
}
public string LocalTime2
{
get { return _localTime2; }
set { UpdateValue(ref _localTime2, value); }
}
public string LocalTime3
{
get { return _localTime3; }
set { UpdateValue(ref _localTime3, value); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void UpdateValue<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(field, value))
{
field = value;
OnPropertyChanged(propertyName);
}
}
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
switch (propertyName)
{
case "TimeZone1":
LocalTime1 = Convert(TimeZone1);
break;
case "TimeZone2":
LocalTime2 = Convert(TimeZone2);
break;
case "TimeZone3":
LocalTime3 = Convert(TimeZone3);
break;
case "LocalTime1":
TryUpdateTime(LocalTime1, TimeZone1);
break;
case "LocalTime2":
TryUpdateTime(LocalTime2, TimeZone2);
break;
case "LocalTime3":
TryUpdateTime(LocalTime3, TimeZone3);
break;
case "Time":
LocalTime1 = Convert(TimeZone1);
LocalTime2 = Convert(TimeZone2);
LocalTime3 = Convert(TimeZone3);
break;
}
}
private void TryUpdateTime(string timeText, TimeZoneInfo timeZone)
{
DateTime time;
if (DateTime.TryParseExact(timeText, _ktimeFormat, null, DateTimeStyles.None, out time))
{
Time = TimeZoneInfo.ConvertTime(time, timeZone, TimeZoneInfo.Utc);
}
}
private string Convert(TimeZoneInfo timeZone)
{
return TimeZoneInfo.ConvertTime(Time, timeZone).ToString(_ktimeFormat);
}
}
This version of the view model includes the formatted text values. Rather than using a converter to format, everything is done here in response to property change notifications that are being raised by the view model itself.
In this version, the view model does get a lot more complicated. But it's all very straightforward, easily understood code. And the XAML winds up a lot simpler:
<Window x:Class="TestSO38517212BindTimeZoneAndTime.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:l="clr-namespace:TestSO38517212BindTimeZoneAndTime"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:ViewModel/>
</Window.DataContext>
<Window.Resources>
<ObjectDataProvider x:Key="timezone"
ObjectType="{x:Type s:TimeZoneInfo}"
MethodName="GetSystemTimeZones">
</ObjectDataProvider>
<p:Style TargetType="ComboBox">
<Setter Property="Width" Value="200"/>
</p:Style>
<p:Style TargetType="TextBox">
<Setter Property="Width" Value="120"/>
</p:Style>
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding Time}"/>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="comboBox1" ItemsSource="{Binding Source={StaticResource timezone}}" SelectedValue="{Binding TimeZone1}"/>
<TextBox Text="{Binding LocalTime1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="comboBox2" ItemsSource="{Binding Source={StaticResource timezone}}" SelectedValue="{Binding TimeZone2}"/>
<TextBox Text="{Binding LocalTime2, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="comboBox3" ItemsSource="{Binding Source={StaticResource timezone}}" SelectedValue="{Binding TimeZone3}"/>
<TextBox Text="{Binding LocalTime3, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</StackPanel>
</Window>
Either of these should directly address the issue you're asking about, i.e. allowing changes in one of multiple values that are derived from a single value being converted to be propagated back to the other values. But if neither of those suit you, you have a number of other options.
One of the most obvious is to simply subscribe to the appropriate property-changed events in each control and then explicitly copy back to the other controls the values you want. IMHO that would be very inelegant, but it wouldn't necessarily require the use of the view-model paradigm, and so it could be argued that would be more consistent with your original example.
Another approach would be to make your converter much more heavyweight, by making it inherit DependencyObject so that it can have a dependency property bound as the target of the time zone value. You would still need to also use the IMultiBindingConverter approach to set the target Text property, but this would allow for a less hacky way to make sure the time zone information is available in the ConvertBack().
You can see an example of this approach in this answer to Get the Source value in ConvertBack() method for IValueConverter implementation in WPF binding. Note that with this approach, each binding will require its own separate instance of the converter. No sharing as a resource.

Data Template not shown

A listbox data template doesn't show and I cannot figure out why.
If I don't use a DataTemplate and copy the contents into the control section itself, it's fine.
I don't do very much binding in XAML, I usually do it all in code. What did I do wrong?
XAML
<UserControl x:Class="Cis.CustomControls.CisArrivalsPanel"
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" Height="296" Width="876">
<UserControl.Resources>
<DataTemplate x:Key="DataTemplate">
<ListBoxItem>
<StackPanel>
<TextBlock Background="Blue" Text="{Binding Path=StationName}" />
<TextBlock Background="Brown" Text="{Binding Path=ArrivalPlatform}" />
</StackPanel>
</ListBoxItem>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation="Horizontal">
<ListBox Width="487" Margin="0,66,0,33" ItemTemplate="{StaticResource DataTemplate}">
</ListBox>
</StackPanel>
</Grid>
</UserControl>
CS
public partial class CisArrivalsPanel : UserControl
{
public CisArrivalsPanel()
{
InitializeComponent();
this.DataContext = new ArrivalRowItem();
}
}
Model
public class ArrivalRowItem : INotifyPropertyChanged
{
public ArrivalRowItem()
{
this.StationName = "Lincoln";
this.ArrivalPlatform = "1";
}
private string _stationName;
public string StationName
{
get
{
return _stationName;
}
set
{
_stationName = value;
NotifyPropertyChanged("StationName");
}
}
private string _arrivalPlatform;
public string ArrivalPlatform
{
get
{
return _arrivalPlatform;
}
set
{
_arrivalPlatform = value;
NotifyPropertyChanged("ArrivalPlatform");
}
}
private DateTime _arrivalDateTime;
public DateTime ArrivalDateTime
{
get
{
return _arrivalDateTime;
}
set
{
_arrivalDateTime = value;
NotifyPropertyChanged("ArrivalDateTime");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
You have everything set up, but you don't actually have any data.
ListBox, like other ItemsControls acts against a collection of data, and generates an instance of the template for each item it finds.
Given that you haven't set ItemsSource or populated any collection I can see, you need to create a collection (probably an ObservableCollection) and set the ItemsSource to it via binding. Then add some items to it, and the ListBox will display them!

How to bind a command and data in a single control?

I am writing a custom control for a project. The control will hold a dropdown combobox and a datagrid. This control will be used throughout the project. On the control I am exposing the SelectedDate and the SelectedDateChangedCommand as dependency properties. When the user selects a date the main programs Model would update it's list of information for the relevant date.
When I use that control in my project the data (SelectedDate) should come from the model but the command (SelectedDateChangedCommand) should come from the ViewModel. How do I bind the data to the Model and the command to the ViewModel?
Basically, if I want to bind the content AND command of the same button, what do I do?
EDIT:
Ok, I don't think I'm explaining this very well. I'm not using a framework or template or anything like that. Maybe my problem was mentioning the UserControl. My issue is not in writing the UserControl. My issue is from the outside of the control. I don't have a code example because that is my question: how do I do this? If I were to have some sort of code it would be like this:
<Button Content="{Binding Model.SelectedDate]" Command="{Binding ViewModel.SelectedDateChanged}" />
How do I bind two properties on a control that are on two different classes?
Wow, even asking questions is difficult in WPF. :)
Ok if I understand you, you want to create a cc. Then Run bind to DateTime to your SelectedDate on your vm, and invoke a command from the control to the vm? I made a simple example for you here, I hope I understood you correctly. I simplified this example by just using a datepicker. I am using galasoft MVVM Light here. Change the content to what you want. Hope I didn't get you completely wrong :)
Generic.xaml
<Style TargetType="{x:Type local:YourCustControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:YourCustControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid >
<DatePicker x:Name="PART_DatePicker"
SelectedDate="{Binding Path=YourDateTime, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:YourCustControl}}}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
CustControl
[TemplatePart(Name = "PART_DatePicker", Type = typeof (DatePicker))]
public class YourCustControl : Control
{
public static readonly DependencyProperty SelectedDateChangedCommandProperty = DependencyProperty.Register("SelectedDateChangedCommand", typeof (ICommand), typeof (YourCustControl), new PropertyMetadata(null));
public static readonly DependencyProperty YourDateTimeProperty = DependencyProperty.Register("YourDateTime", typeof (DateTime), typeof (YourCustControl), new PropertyMetadata(null));
private DatePicker datePicker;
static YourCustControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof (YourCustControl), new FrameworkPropertyMetadata(typeof (YourCustControl)));
}
public ICommand SelectedDateChangedCommand
{
get { return (ICommand) GetValue(SelectedDateChangedCommandProperty); }
set { SetValue(SelectedDateChangedCommandProperty, value); }
}
public DateTime YourDateTime
{
get { return (DateTime) GetValue(YourDateTimeProperty); }
set { SetValue(YourDateTimeProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
datePicker = (DatePicker) Template.FindName("PART_DatePicker", this);
if (datePicker != null)
{
datePicker.SelectedDateChanged += datePicker_SelectedDateChanged;
}
}
private void datePicker_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
{
// Execute the command
if (SelectedDateChangedCommand != null && SelectedDateChangedCommand.CanExecute(e) && !e.Handled)
SelectedDateChangedCommand.Execute(e);
}
}
ViewModel
// replace with whatever, like extend Galasoft's ViewModelBase
public class YourViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator] // Remove if no R#
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class YourViewModel : YourViewModelBase
{
private DateTime dateTime;
public DateTime DateTime
{
get { return dateTime; }
set
{
if (value.Equals(dateTime)) return;
dateTime = value;
OnPropertyChanged();
}
}
public ICommand SelectedDateChangedCommand { get; set; }
public YourViewModel()
{
SelectedDateChangedCommand = new RelayCommand<SelectionChangedEventArgs>(OnSelectedDateChanged);
}
private void OnSelectedDateChanged(SelectionChangedEventArgs e)
{
if (e != null)
e.Handled = true; // dirty hack
// do stuff here
}
}
Finally your xaml
<Grid>
<local:YourCustControl SelectedDateChangedCommand="{Binding SelectedDateChangedCommand}"
YourDateTime="{Binding DateTime, Mode=TwoWay}"/>
</Grid>
Hope it helps!
Cheers

Categories

Resources