IValueConverter - 使用源和转换器在相同的绑定/相同控制中的源和转换器

时间:2016-07-22 02:26:22

标签: c# wpf

我有一组这样的组合框和文本框:

C1 T1

C2 T2

C3 T3

我实现了一个IValueConverter来设置C1中的TimeZone并获得T1中的相应时间。其他对也一样。

我想要做的是:如果用户手动更改T1中的时间,则T2和T3中的时间必须相应于T1以及TimeZone更改。

T1不是参考。如果任何文本框的值已更改,则所有其他文本框也必须更改。

可能会发生以下变化:

  1. 如果在Combobox中更改了TimeZone

  2. 如果用户通过在文本框中键入

  3. 来手动更改时间

    以下是我的完整代码:

     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吗?

1 个答案:

答案 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的实现,该实现将组合框索引(即Index1Index2Index3视为输入)和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中查看此方法的示例。请注意,使用此方法,每个绑定都需要其自己的转换器单独实例。不作为资源共享。