我们在WPF数据绑定(上)中研究了数据绑定的一些基础知识,以及如何建立不同绑定源的几种情况。本文继续讲一些绑定的有关内容。
1.ObjectDataProvider 对象
上文中我们使用了XmlDataProvider,把xml数据作为数据源提供给Binding。根据名字我们可以显然的知道,ObjectDataProvider是用来包装和创建一个对象来提供给Binding。那我们一般在什么情况下会使用这个对象来作为绑定源呢?
我们知道一般情况下,要绑定的源对象通过属性将数据暴露出来,然后Binding通过Source和Path建立绑定。但是
我们很难保证所有的业务类都是为WPF项目准备的。譬如说我们需要绑定一个类中的方法的返回值,因为它不是类的属性,那么如何实现绑定呢。而重新设计底层类的风险和成本会比较高,So可以使用ObjectDataProvider 来实现方法作为数据绑定源。我们来写一个例子看一下。
首先我们写一个BasketBallPlayer类,类中实现一个方法。计算球员一个赛季的平均分。代码如下:
public class BasketBallPlayer : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string name;
public string Name// 每个球员的名字
{
get
{
return name;
}
set
{
name = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
}
// 计算一个球员场均得分的情况
public string AveragePoint(string game, string point)
{
double x = 0;
double y = 0;
double z = 0;
if (double.TryParse(game, out x) && double.TryParse(point, out y))
{
z = x / (y * 1.0);
return z.ToString();
}
return "Input Error!";
}
}
我们在XAML中定义三个对话框
<StackPanel>
<TextBox Margin="5" x:Name="textbox1" Height="25"/>
<TextBox Margin="5" x:Name="textbox2" Height="25"/>
<TextBox Margin="5" x:Name="textbox3" Height="25"/>
</StackPanel>
图:
我们希望可以做到,输入球员得分和球员参赛场数得到球员的平均分, 那么如何做到呢?TextBox需要绑定到方法的返回值。OK,ObjectDataProvider派上用场了。
ObjectDataProvider的几个属性:
![]() | MethodName | 获取或设置要调用的方法的名称。 |
![]() | MethodParameters | 获取要传递给该方法的参数列表。 |
![]() | ObjectInstance | 获取或设置用作绑定源的对象。 |
![]() | ObjectType | 获取或设置要创建其实例的对象的类型。 |
![]() | Data | 获取基础数据对象。 (继承自 DataSourceProvider。) |
//创建并且配置ObjectDataProvider对象
ObjectDataProvider myodp = new ObjectDataProvider();
myodp.ObjectInstance = new BasketBallPlayer(); //设置对象实例
myodp.MethodName = "AveragePoint"; // 设置调用的方法
myodp.MethodParameters.Add("100"); //添加方法的参数
myodp.MethodParameters.Add("200");
//以ObjectDataProvider为Source创建Binding,Path为参数列表的第一个参数,并配置一些属性
Binding bindToArg1 = new Binding("MethodParameters[0]")
{
Source = myodp,
BindsDirectlyToSource = true,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged // 设置触发时间
};
//已ObjectDataProvider对象为Binding源
Binding bindToArg2 = new Binding("MethodParameters[1]")
{
Source = myodp,
BindsDirectlyToSource = true,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
Binding bindingResult = new Binding(".") { Source = myodp };
//绑定到关联UI元素上
this.textbox1.SetBinding(TextBox.TextProperty, bindToArg1);
this.textbox2.SetBinding(TextBox.TextProperty, bindToArg2);
this.textbox3.SetBinding(TextBox.TextProperty, bindingResult);
我们来分析下运行过程,首先将2个TextBox的内容分别绑定myodp上,Path为参数列表的参数,当内容发生变化,参数也发生变化。将最后一个TextBox的内容绑定到myodp,Path为空。因为这个对象本身就是一个数据。
运行结果:
注意一下这个属性:BindsDirectlyToSource(字面意思 直接绑定到源,也就是ObjectDataProvider对象)
获取或设置一个值,该值指示是否计算相对于数据项或 DataSourceProvider 对象的 Path。
BindsDirectlyToSource = true的意思就是告诉Binding对象 从UI元素(这里是两个TextBox)收集到的元素写入直接Source(即myodp这个ObjectDataProvider对象,而不是被它包装的BasketBallPlayer类)
可以把 这两个属性改为 false试一试,初始运行是这个样子:(因为没有直接绑定到源,所以没有显示出来我们初始的时候添加的参数)
创建包装对象的另外一种方法,(这是在XAML代码中)
<ObjectDataProvider MethodName="GetValues"
ObjectType="{x:Type sys:Enum}"
x:Key="AlignmentValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="HorizontalAlignment" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
创建一个ObjectDataProvider对象,MethodName为GetValuses,对象实例类型为Enum,MethodParameters参数为HorizontalAlignment类型。OK 基本的东西就这么多。(和C#代码所需要的是一样的)。
下面我们尝试一下把刚才的绑定在 XAML代码中实现一遍。
首先声明类:
public class BasketBallPlayer
{
// 计算一个球员场均得分的情况
public string AveragePoint(string game, string point)
{
double x = 0;
double y = 0;
double z = 0;
if (double.TryParse(game, out x) && double.TryParse(point, out y))
{
z = x / (y * 1.0);
return z.ToString("F3");
}
return "Input Error!";
}
}
注意函数的参数是String类型,因为从TextBox绑定过来的话是String,如果使用别的类型的话,需要使用类型转换器。
定义资源ObjectDataProvider对象
<Window.Resources>
<ObjectDataProvider ObjectType="{x:Type local:BasketBallPlayer}"
MethodName="AveragePoint"
x:Key="Calculate"
>
<ObjectDataProvider.MethodParameters>
<s:String>1992</s:String>
<s:String>2</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
由于使用了本地的类和 字符串类型,需要添加
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:DataBindingExample"
然后绑定TextBox
<StackPanel>
<TextBox Margin="5" x:Name="textbox1" Height="25" >
<TextBox.Text>
<Binding Source="{StaticResource Calculate}" Path="MethodParameters[0]"
BindsDirectlyToSource="true" UpdateSourceTrigger="PropertyChanged"
Mode="TwoWay" />
</TextBox.Text>
</TextBox>
<TextBox Margin="5" x:Name="textbox2" Height="25"
Text="{Binding Source={StaticResource Calculate},Path=MethodParameters[1],
BindsDirectlyToSource=true, UpdateSourceTrigger=PropertyChanged,
Mode = TwoWay
}"/>
<TextBox Margin="5" x:Name="textbox3" Height="25"
Text="{Binding Source={StaticResource Calculate},Path=.,Mode = TwoWay}"/>
</StackPanel>
注意 前2个TextBox绑定的区别。推荐使用第一种(因为第二种方式容易打错字,而且系统是不会报错的)。
运行结果:
上面我们类中采用的是String的参数,但是我们这个类设计本身的话是希望参数为double类型的。
我们需要来实现一个类型转换器。把输入的String参数转换到Double类型。而且我们希望输入的值是在一定的范围内。所以要加以一些限制
下面来引入Binding对数据的转换与校验
数据转换需要动手写一个Converter类并且让它实现IValueConveter接口。接口定义如下:
public interface IValueConverter
{
// Summary:
// Converts a value.
//
// Parameters:
// value:
// The value produced by the binding source.
//
// targetType:
// The type of the binding target property.
//
// parameter:
// The converter parameter to use.
//
// culture:
// The culture to use in the converter.
//
// Returns:
// A converted value. If the method returns null, the valid null value is used.
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
//
// Summary:
// Converts a value.
//
// Parameters:
// value:
// The value that is produced by the binding target.
//
// targetType:
// The type to convert to.
//
// parameter:
// The converter parameter to use.
//
// culture:
// The culture to use in the converter.
//
// Returns:
// A converted value. If the method returns null, the valid null value is used.
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
数据从Binding的Source流向Target的时候,Convert方法被调用。当从Target流向Source的时候,ConvertBack被调用.我们来自定义使用这个类。
public class DoubleToStringConverter : IValueConverter
{
}
然后手动添加接口代码:
添加代码如下,
public class DoubleToStringConverter : IValueConverter
{
#region IValueConverter Members
//source to target(double to string)
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double d = (double)value;
return d.ToString();
//throw new NotImplementedException();
}
//String to Double
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string s = (string)value;
double x;
if (double.TryParse(s, out x))
return x;
else
return 0.0;
//throw new NotImplementedException();
}
#endregion
}
然后以资源的形式在XAML代码中添加一个实例。
<local:DoubleToStringConverter x:Key="myConvert"/>
在Texbox的Text属性内,引用它。
<Binding Source="{StaticResource Calculate}" Path="MethodParameters[0]"
BindsDirectlyToSource="true" UpdateSourceTrigger="PropertyChanged"
Mode="TwoWay" Converter="{StaticResource myConvert}"/>
<TextBox Margin="5" x:Name="textbox2" Height="25"
Text="{Binding Source={StaticResource Calculate},Path=MethodParameters[1],
BindsDirectlyToSource=true, UpdateSourceTrigger=PropertyChanged,
Mode = TwoWay,Converter={StaticResource myConvert}
}"/>
OK,类型转换这里就实现了。
下面我们希望在输入的时候能够有一个范围,比如说输入0到3000这个范围,则需要设置Binding的ValidationRules属性,这个属性类型是Collection<ValidationRule>
,我们可以为每个Binding设置多个数据校验条件。每个条件都是一个ValidationRule类型对象(抽象类)。我们要做的是创建它的派生类并实现它的Validate方法。方法返回值是ValidationResult类型对象,如果验证通过,需要设置ValidationResult类型对象的IsValid属性为True,没通过需要设置为false,并为ErrorContent属性设置一个合适的消息内容(一般是字符串)
例子:
public class myValidation1 : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
double d = 0;
if(double.TryParse(value.ToString(),out d))
{
if(d <=5000 && d>0)
{
return new ValidationResult(true,null);
}
}
return new ValidationResult(false, "Validation Failed");
}
}
在Text中绑定这个
<local:myValidation1 x:Key="myValidation1"/>
<TextBox.Text>
<Binding Source="{StaticResource Calculate}" Path="MethodParameters[0]"
BindsDirectlyToSource="true" UpdateSourceTrigger="PropertyChanged"
Mode="TwoWay" Converter="{StaticResource myConvert}"
NotifyOnValidationError="True"
>
<Binding.ValidationRules> //这里是添加ValidationRules的一个ValidationRule
<local:myValidation1 ></local:myValidation1>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
但是这样做不能弹出出错的信息,需要写路由事件,这个后续来写~
2.MultiBinding(多路Binding)
有的时候UI需要显示的信息由不止一个数据源来决定,这个时候就需要多路Binding.
一年一度的NBA选秀报名大赛开始了,报名者需要注册填写一些必要的信息才可以报名参加选秀。需要填写的信息如下图所示:
我们看到现在这个提交的按钮是灰的。也就是说现在是不能提交信息的。当你4个文本框都输入内容的时候,我们才可以提交,那么这个按钮的IsEnabled这个属性就由4个数据源来决定的。这个时候就需要用到多路绑定。
xaml代码:
<StackPanel Background="AntiqueWhite" Orientation="Vertical">
<TextBlock Background="BlanchedAlmond" Margin="10" FontWeight="Bold" FontSize="16.5" Text="Welcome To NBA Draft:Please first Sign up "/>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" Width="50" Text="Name:"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="16"
/>
<TextBox Height="30" x:Name="textBoxName" Width="300"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" Width="50" Text="Age:"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="16"
/>
<TextBox Height="30" x:Name="textBoxAge" Width="300"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" Width="50" Text="Phone:"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="16"
/>
<TextBox Height="30" x:Name="textBoxPhone" Width="300"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" Width="50" Text="Email:"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="16"
/>
<TextBox Height="30" x:Name="textBoxEmail" Width="300"/>
</StackPanel>
<Button x:Name="buttonSubmit" HorizontalAlignment="Center" VerticalAlignment="Center"
Content="Submit" Width="50" Height="30" IsEnabled="False" />
</StackPanel>
C#代码:
public Window1()
{
InitializeComponent();
this.SetMultiBinding();
}
private void SetMultiBinding()
{
//准备基础的Binding
Binding bindingName = new Binding("Text") { Source = this.textBoxName };
Binding bindingAge = new Binding("Text") { Source = this.textBoxAge };
Binding bindingPhone = new Binding("Text") { Source = this.textBoxPhone };
Binding bindingEmail = new Binding("Text") { Source = this.textBoxEmail };
//准备MultiBinding
MultiBinding myMultiBinding = new MultiBinding() { Mode = BindingMode.OneWay };
myMultiBinding.Bindings.Add(bindingName);
myMultiBinding.Bindings.Add(bindingAge);
myMultiBinding.Bindings.Add(bindingPhone);
myMultiBinding.Bindings.Add(bindingEmail);
myMultiBinding.Converter = new myMultiBindingConverter();
this.buttonSubmit.SetBinding(Button.IsEnabledProperty, myMultiBinding);
}
Converter类为:
public class myMultiBindingConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((values[0].ToString().Length != 0) && (values[1].ToString().Length != 0)
&&(values[2].ToString().Length != 0) && (values[3].ToString().Length != 0))
{
return true;
}
else
return false;
//throw new NotImplementedException();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
运行结果:
需要注意的地方 MultiBinding对于添加子级Binding的顺序是敏感的。因为这个顺序决定了汇集到Converter里数据的顺序
Convert函数中最重要的就是它的values参数。这个参数是一个数组,这个数组里包含的就是从一对一子Binding里送来的值(在我们的程序里,就是4个TextBox的Text属性值)。数组是可被索引的,这就意味着values里面的值是有顺序的!那么这个顺序是什么呢?这个顺序就是你调用MultiBinding.Bindings.Add(...)添加子Binding顺序。本例中,values[0]对应的是textBox1.Text属性值、values[1]对应的是textBox2.Text属性值、values[2]对应的是textBox3.Text属性值、values[3]对应的是textBox4.Text属性值。
另外本例的Converter基接口是IMultiValueConverter,而不是IValueConverter。这个接口仍然只有两个函数需要实现一下。又因为我们这次使用的是OneWay模式,所以,代码中只有Convert函数中包含逻辑。