我有一组这样的组合框和文本框:
C1 T1
C2 T2
C3 T3
我实现了一个IValueConverter来设置C1中的TimeZone并获得T1中的相应时间。其他对也一样。
我想要做的是:如果用户手动更改T1中的时间,则T2和T3中的时间必须相应于T1以及TimeZone更改。
T1不是参考。如果任何文本框的值已更改,则所有其他文本框也必须更改。
可能会发生以下变化:
如果在Combobox中更改了TimeZone
如果用户通过在文本框中键入
以下是我的完整代码:
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>
问题:如何使用转换方法将相应的更改级联到其他文本框,与combox的方式相同? 我可以使用TextChanged方法捕获引用文本框中的更改,我可以添加几行代码来进行这些更改,但我想使用IValueConverter。我可以在文本框的相同绑定中使用Source和Converter吗?
答案 0 :(得分:1)
如果没有一个好的Minimal, Complete, and Verifiable code example能够清楚地显示您到目前为止所做的事情,那么很难提供准确的信息。但根据您所描述的内容,这里的主要问题似乎是您没有使用WPF设计用于的常规&#34;视图模型和基于技术的技术。此外,您将Text
属性绑定到时区而不是时间,因此当Text
属性更改时,WPF想要更新的内容实际上是组合框选择而不是时间本身。
您需要做的第一件事是,创建一个代表您想要显示的实际状态的视图模型类,而不是让您的控件将彼此引用,然后将其用于您的窗口中的DataContext
,将适当的属性绑定到特定控件。
那些适当的属性是什么?那么,根据你的描述,你实际只有四个: 1)实际时间, 2)到 4)三个时区你想要处理。
所以,像这样:
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));
}
}
}
}
(人们经常将PropertyChanged
事件和UpdateValue()
方法封装在一个可以重用于所有视图模型类型的基类中。)
然后,您编写IMultiValueConverter
的实现,该实现将组合框索引(即Index1
,Index2
或Index3
视为输入)和Time
属性值使用这两个值来生成文本框的时区转换值,该值使用这两个值和转换器绑定。
转换器的Convert()
方法将执行上述转换。然后,您需要使ConvertBack()
方法使用适当的组合框值转换回UTC时间。
不幸的是,这里有点皱纹。您的转换器通常无法访问该值。 IMultiValueConverter.ConvertBack()
方法仅获取绑定的目标值,并且期望从该值转换回绑定的源值。它的设计不允许您根据其他源值和目标值更新一个源值。
围绕这个限制有很多方法,但我所知道的都不是很优雅。
一个选项完全按照我上面显示的方式使用视图模型。诀窍是,您需要通过ConverterParameter
引用与绑定ComboBox
属性关联的Text
,以便ConvertBack()
方法可以使用当前选定的值(您无法将当前选定的值本身作为ConverterParamater
值传递,因为ConverterParameter
不是依赖项属性,因此无法成为属性绑定的目标)。
通过这种方式,你可能有一个如下所示的转换器:
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 };
}
}
看起来像这样的XAML:
<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>
这在运行时可以正常工作。但是你会得到设计时_和#34;对象引用没有设置为对象的实例&#34;由于在{x:Reference ...}
分配中使用了ConverterParameter
,因此出现错误消息。对某些人来说,这是一个小小的不便,但我觉得这是一个巨大的烦恼,我愿意付出相当大的努力来避免它。 :)
所以,这是一个完全不同的方法,它完全放弃了转换器并将所有逻辑放在视图模型中:
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);
}
}
此版本的视图模型包含格式化文本值。不是使用转换器进行格式化,而是在此处完成所有操作以响应视图模型本身引发的属性更改通知。
在这个版本中,视图模型确实变得更加复杂。但它是非常简单易懂的代码。而且XAML变得更加简单:
<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>
这些中的任何一个都应直接解决您所询问的问题,即允许从转换的单个值派生的多个值之一的更改传播回其他值。但如果这些都不适合你,你还有其他一些选择。
最明显的一个是简单地在每个控件中订阅适当的属性更改事件,然后显式地将所需的值复制回其他控件。恕我直言,非常不优雅,但它不一定需要使用视图模型范例,因此可以认为这将与您的原始示例更加一致。
另一种方法是让你的转换器更加重量级,方法是让它继承DependencyObject
,以便它可以将依赖项属性绑定为时区值的目标。您仍然需要使用IMultiBindingConverter
方法来设置目标Text
属性,但这样可以避免在确保ConvertBack()
中的时区信息可用时采用较少的方法
您可以在this answer to Get the Source value in ConvertBack() method for IValueConverter implementation in WPF binding中查看此方法的示例。请注意,使用此方法,每个绑定都需要其自己的转换器单独实例。不作为资源共享。