在现代软件开发中,将业务逻辑与用户界面(UI)分离是一种常见的设计模式。这种分离有助于提高代码的可维护性和可扩展性。本文将通过一个简单的示例,展示如何在WinFX框架下创建一个自定义控件,并实现模板的自定义,从而将业务逻辑与UI分离。
首先,需要创建两个项目:一个WinFX Windows应用程序和一个WinFX自定义控件库。
WinFX Windows应用程序:这个应用程序将用于测试自定义控件的功能。
WinFX自定义控件库:这个库将包含自定义的时钟控件。
接下来,需要在自定义控件库中添加一个新的自定义控件,并删除默认的UserControl1.xaml文件。同时,需要在Windows应用程序项目中添加对自定义控件库的引用。
自定义控件的核心是添加一个依赖属性(DependencyProperty),该属性将始终包含当前的日期和时间。控件的模板可以绑定到这个属性上。
public class Clock : Control
{
public static readonly DependencyProperty DateTimeProperty =
DependencyProperty.Register("DateTime", typeof(DateTime), typeof(Clock),
new PropertyMetadata(DateTime.Now, OnDateTimeInvalidated));
public DateTime DateTime
{
get { return (DateTime)GetValue(DateTimeProperty); }
private set { SetValue(DateTimeProperty, value); }
}
public static readonly RoutedEvent DateTimeChangedEvent =
EventManager.RegisterRoutedEvent("DateTimeChanged",
RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(Clock));
protected virtual void OnDateTimeChanged(DateTime oldValue, DateTime newValue)
{
RoutedPropertyChangedEventArgs args =
new RoutedPropertyChangedEventArgs(oldValue, newValue);
args.RoutedEvent = Clock.DateTimeChangedEvent;
RaiseEvent(args);
}
private static void OnDateTimeInvalidated(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Clock clock = (Clock)d;
DateTime oldValue = (DateTime)e.OldValue;
DateTime newValue = (DateTime)e.NewValue;
clock.OnDateTimeChanged(oldValue, newValue);
}
private DispatcherTimer timer;
static Clock()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Clock), new FrameworkPropertyMetadata(typeof(Clock)));
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
UpdateDateTime();
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(1000 - DateTime.Now.Millisecond);
timer.Tick += new EventHandler(Timer_Tick);
timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
UpdateDateTime();
timer.Interval = TimeSpan.FromMilliseconds(1000 - DateTime.Now.Millisecond);
timer.Start();
}
private void UpdateDateTime()
{
this.DateTime = System.DateTime.Now;
}
}
在上述代码中,定义了一个名为Clock的自定义控件类。这个类包含一个DateTime依赖属性,该属性用于存储当前的日期和时间。同时,使用DispatcherTimer定时器来更新这个属性的值。
接下来,需要为自定义控件创建一个默认模板。这个模板将在themes文件夹中的generic.xaml文件中定义。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlLibrary">
<Style TargetType="{x:Type local:Clock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Clock}">
<TextBlock Name="tblClock" Text="{Binding Path=DateTime, RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
在上述XML代码中,定义了一个TextBlock控件,用于显示当前的DateTime属性值。这个控件将作为Clock控件的默认模板。
现在,可以在Windows应用程序中使用自定义控件了。
<Window x:Class="WindowsApplication.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:customControl="clr-namespace:CustomControlLibrary; assembly=CustomControlLibrary"
Title="WindowsApplication" Height="487" Width="412">
<StackPanel HorizontalAlignment="Center">
<customControl:Clock Name="customControlClock" />
</StackPanel>
</Window>
在上述XML代码中,定义了一个名为Window1的窗口,并在其中添加了一个Clock控件。这个控件将使用在generic.xaml文件中定义的默认模板。
通过覆盖默认模板,可以完全改变控件的外观和感觉。
<StackPanel.Resources>
<local:SecondsConverter x:Key="secondsConverter" />
<local:MinutesConverter x:Key="minutesConverter" />
<local:HoursConverter x:Key="hoursConverter" />
<local:WeekdayConverter x:Key="weekdayConverter" />
<Style x:Key="AnalogClock" TargetType="{x:Type customControl:Clock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type customControl:Clock}">
...
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
在上述XML代码中,定义了一些转换器(Converter),用于将当前的DateTime属性值转换为模拟时钟的指针角度。然后,定义了一个名为AnalogClock的样式,并将其应用于Clock控件。
在模板中,使用了一些椭圆和矩形来绘制时钟的表盘。然后,将时钟的指针绑定到自定义控件的DateTime依赖属性上。由于RotateTransform的角度不能直接绑定到DateTime数据类型,需要使用转换器对象。
<Rectangle x:Name="SecondHand" Canvas.Top="4" Canvas.Left="49" Fill="Red" Width="1" Height="47">
<Rectangle.RenderTransform>
<RotateTransform Angle="{Binding Path=DateTime, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource secondsConverter}}" CenterX="0.5" CenterY="47" />
</Rectangle.RenderTransform>
</Rectangle>
在上述XML代码中,定义了一个名为SecondHand的矩形控件,并将其渲染转换(RotateTransform)的角度绑定到DateTime属性上。使用了一个名为secondsConverter的转换器,将DateTime属性的秒部分转换为秒针的角度。
最后,需要在Window1.xaml的代码隐藏文件中添加转换器。
[ValueConversion(typeof(DateTime), typeof(int))]
public class SecondsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
DateTime date = (DateTime)value;
return date.Second * 6;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
在上述C#代码中,定义了一个名为SecondsConverter的转换器类。这个类实现了IValueConverter接口,并定义了Convert方法,用于将DateTime属性的秒部分转换为秒针的角度。
最后,需要将新模板应用到Clock控件上。
<customControl:Clock Name="customControlAnalogClock" Style="{StaticResource AnalogClock}" />