|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionThis is Part 2 in a series of articles on writing WPF Business Applications in VB.NET using Visual Studio 2008. This series tries to take a complex form or application and deliver articles on small pieces so that each piece can be understood. If you have not yet read Part 1, please take a few minutes and read it. This series builds on previous articles and does not repeat information. Note: On 21 Feb 2008, I rewrote Part 1 of the series making large changes to the skinning and button control. If you previously read this article before 21 Feb 2008, please go back and review it.
New To WPF?There are many fine WPF authors here on Code Project with great insight and code. If you are new to WPF can I suggest that you bookmark this article and take the time to read the foundational tutorials that Code Project MVP's Josh Smith and Sacha Barber have authored. In addition to their tutorials, each have many WPF articles here on Code Project and their blogs with sample code. I should also mention that the SDK Team has put a lot of work into providing a lot of good, simple examples in the WPF SDK that are easy to learn from. When you installed Visual Studio 2008, this was also installed. Take advantage of this free code. There is also a new Framework 3.5 SDK that was recently released. There is an issue with the install package and when you install this new 3.5 SDK, XAML intellisense will quit working. Please read my blog post for the registry entry fix. Article Highlights
FormNotification ControlBeing a long time ASP.NET developer I like to give the web page visitor a visual cue when there are errors on the page or the requested operation was successful. ASP.NET has a The control is located in close proximity to the In the below series of images the The above image shows a simple form with two fields that must be filled in. The The The In the above image the user clicked the In the above image the user has clicked the The above image shows the Watermark feature of the In the above image the user has just pressed the Save The FormNotification Control Requirements
In keeping with good WPF custom control design, I have provided a number of dependency properties to control the behavior of the I didn't supply a FormNotification ControlTemplateAt first glance the below XAML You will notice how the <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Core_WPF="clr-namespace:Core.WPF">
<Style
TargetType="{x:Type Core_WPF:FormNotification}">
<Style.Resources>
<Core_WPF:StringLengthToBooleanConverter
x:Key="stringLengthToBooleanConverter" />
</Style.Resources>
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type Core_WPF:FormNotification}">
<Grid
Width="Auto"
Height="Auto"
Background="{Binding Path=Background,
RelativeSource={RelativeSource TemplatedParent}}">
<Canvas
x:Name="canvasEntryErrors"
HorizontalAlignment="Stretch"
Visibility="Collapsed">
<Expander
x:Name="PART_Expander"
Foreground="{Binding Path=ErrorHeaderForeground,
RelativeSource={RelativeSource TemplatedParent}}"
Header="{Binding Path=ErrorHeaderText,
RelativeSource={RelativeSource TemplatedParent}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid
Background="{x:Null}" />
</Expander>
</Canvas>
<TextBlock
Text="{Binding Path=NotificationMessage,
RelativeSource={RelativeSource TemplatedParent}}"
Foreground="{Binding Path=NotificationMessageForeground,
RelativeSource={RelativeSource TemplatedParent}}"
Background="{Binding Path=NotificationMessageBackground,
RelativeSource={RelativeSource TemplatedParent}}"
Margin="5,5,5,5"
TextWrapping="Wrap"
x:Name="txtNofificationMessage"
VerticalAlignment="Center"
Visibility="Collapsed"
HorizontalAlignment="Stretch" />
<TextBlock
Text="{Binding Path=WatermarkMessage,
RelativeSource={RelativeSource TemplatedParent}}"
Foreground="{Binding Path=WatermarkMessageForeground,
RelativeSource={RelativeSource TemplatedParent}}"
Background="{Binding Path=WatermarkMessageBackground,
RelativeSource={RelativeSource TemplatedParent}}"
Margin="5,5,5,5"
TextWrapping="Wrap"
x:Name="txtWatermarkMessage"
Visibility="Visible"
HorizontalAlignment="Stretch"
FontStyle="Italic" />
</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition
Property="NotificationMessage"
Value="" />
<Condition
Property="ErrorMessage"
Value="" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter
Property="Visibility"
Value="Visible"
TargetName="txtWatermarkMessage" />
<Setter
Property="Visibility"
Value="Collapsed"
TargetName="txtNofificationMessage" />
<Setter
Property="Visibility"
Value="Collapsed"
TargetName="canvasEntryErrors" />
</MultiTrigger.Setters>
</MultiTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition
Binding="{Binding Path=ErrorMessage,
RelativeSource={RelativeSource Self},
Converter={StaticResource stringLengthToBooleanConverter}}"
Value="False" />
<Condition
Binding="{Binding Path=NotificationMessage,
RelativeSource={RelativeSource Self},
Converter={StaticResource stringLengthToBooleanConverter}}"
Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter
Property="Visibility"
Value="Collapsed"
TargetName="txtWatermarkMessage" />
<Setter
Property="Visibility"
Value="Visible"
TargetName="txtNofificationMessage" />
<Setter
Property="Visibility"
Value="Collapsed"
TargetName="canvasEntryErrors" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition
Binding="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource stringLengthToBooleanConverter},
Path=ErrorMessage}"
Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter
Property="Visibility"
Value="Collapsed"
TargetName="txtWatermarkMessage" />
<Setter
Property="Visibility"
Value="Collapsed"
TargetName="txtNofificationMessage" />
<Setter
Property="Visibility"
Value="Visible"
TargetName="canvasEntryErrors" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
TriggersThe above MultiTriggerThis first triggers tests if the MultiDataTriggerTriggers by themselves do not allow developers to test to see if the property value is greater than or less than a certain value. However, by using a Have a look at the above second and third triggers. By using a The Imports System.ComponentModel
Namespace WPF
<TemplatePart(Name:="PART_Expander", Type:=GetType(Expander))> _
Public Class FormNotification
Inherits System.Windows.Controls.Control
#Region " Declarations "
Private WithEvents _objErrorsExpander As Expander
Private WithEvents _objErrorsExpanderAdornerLayer As AdornerLayer
Private WithEvents _objExpandedTimer As System.Timers.Timer
Private WithEvents _objTextBlockAdorner As TextBlockAdorner
Private Delegate Sub ExpanderDelegate()
#End Region
#Region " Shared Properties "
Public Shared AutoCollapseTimeoutProperty As DependencyProperty = _
DependencyProperty.Register("AutoCollapseTimeout", GetType(Double), _
GetType(FormNotification), New PropertyMetadata(2.0), New _
ValidateValueCallback(AddressOf IsAutoCollapseTimeoutValid))
Public Shared ErrorHeaderForegroundProperty As DependencyProperty = _
DependencyProperty.Register("ErrorHeaderForeground", GetType(Brush), _
GetType(FormNotification), New PropertyMetadata(New SolidColorBrush( _
Color.FromArgb(255, 208, 0, 0))))
Public Shared ErrorHeaderTextProperty As DependencyProperty = _
DependencyProperty.Register("ErrorHeaderText", GetType(String), _
GetType(FormNotification), New PropertyMetadata("Edit Errors"))
Public Shared ErrorMessageProperty As DependencyProperty = _
DependencyProperty.Register("ErrorMessage", GetType(String), GetType( _
FormNotification), New PropertyMetadata(String.Empty, New _
PropertyChangedCallback(AddressOf OnErrorMessageChanged)))
Public Shared ErrorPopUpBackgroundProperty As DependencyProperty = _
DependencyProperty.Register("ErrorPopUpBackground", GetType(Brush), _
GetType(FormNotification), New PropertyMetadata(New SolidColorBrush( _
Color.FromArgb(255, 253, 240, 151))))
Public Shared ErrorPopUpForegroundProperty As DependencyProperty = _
DependencyProperty.Register("ErrorPopUpForeground", GetType(Brush), _
GetType(FormNotification), New PropertyMetadata(New SolidColorBrush( _
Colors.Black)))
Public Shared NotificationMessageBackgroundProperty As DependencyProperty _
= DependencyProperty.Register("NotificationMessageBackground", GetType( _
Brush), GetType(FormNotification), New PropertyMetadata(New _
SolidColorBrush(Colors.LightGray)))
Public Shared NotificationMessageForegroundProperty As DependencyProperty _
= DependencyProperty.Register("NotificationMessageForeground", GetType( _
Brush), GetType(FormNotification), New PropertyMetadata(New _
SolidColorBrush(Colors.Blue)))
Public Shared NotificationMessageProperty As DependencyProperty = _
DependencyProperty.Register("NotificationMessage", GetType(String), _
GetType(FormNotification), New PropertyMetadata(String.Empty, New _
PropertyChangedCallback(AddressOf OnNotificationMessageChanged)))
Public Shared WatermarkMessageBackgroundProperty As DependencyProperty = _
DependencyProperty.Register("WatermarkMessageBackground", GetType( _
Brush), GetType(FormNotification))
Public Shared WatermarkMessageForegroundProperty As DependencyProperty = _
DependencyProperty.Register("WatermarkMessageForeground", GetType( _
Brush), GetType(FormNotification), New PropertyMetadata(New _
SolidColorBrush(Colors.Gray)))
Public Shared WatermarkMessageProperty As DependencyProperty = _
DependencyProperty.Register("WatermarkMessage", GetType(String), _
GetType(FormNotification), New PropertyMetadata(String.Empty))
#End Region
#Region " Properties "
<Category("Custom"), Description("Number of seconds the error pop remain...")> _
Public Property AutoCollapseTimeout() As Double
Get
Return CType(GetValue(AutoCollapseTimeoutProperty), Double)
End Get
Set(ByVal value As Double)
SetValue(AutoCollapseTimeoutProperty, value)
If _objExpandedTimer IsNot Nothing Then
_objExpandedTimer.Interval = value
End If
End Set
End Property
<Category("Custom"), Description("Error header text foreground brush.")> _
Public Property ErrorHeaderForeground() As Brush
Get
Return CType(GetValue(ErrorHeaderForegroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(ErrorHeaderForegroundProperty, value)
End Set
End Property
<Category("Custom"), Description("Error header text that is displayed when there ...")> _
Public Property ErrorHeaderText() As String
Get
Return CType(GetValue(ErrorHeaderTextProperty), String)
End Get
Set(ByVal value As String)
SetValue(ErrorHeaderTextProperty, value)
End Set
End Property
<Category("Custom"), Description("Error message that is displayed in the expander ...")> _
Public Property ErrorMessage() As String
Get
Return CType(GetValue(ErrorMessageProperty), String)
End Get
Set(ByVal value As String)
SetValue(ErrorMessageProperty, value)
End Set
End Property
<Category("Custom"), Description("Error message pop up background brush.")> _
Public Property ErrorPopUpBackground() As Brush
Get
Return CType(GetValue(ErrorPopUpBackgroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(ErrorPopUpBackgroundProperty, value)
End Set
End Property
<Category("Custom"), Description("Error message pop up forground brush.")> _
Public Property ErrorPopUpForeground() As Brush
Get
Return CType(GetValue(ErrorPopUpForegroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(ErrorPopUpForegroundProperty, value)
End Set
End Property
<Category("Custom"), Description("Notification message text. If this property is ...")> _
Public Property NotificationMessage() As String
Get
Return CType(GetValue(NotificationMessageProperty), String)
End Get
Set(ByVal value As String)
SetValue(NotificationMessageProperty, value)
End Set
End Property
<Category("Custom"), Description("Notification message pop up background brush.")> _
Public Property NotificationMessageBackground() As Brush
Get
Return CType(GetValue(NotificationMessageBackgroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(NotificationMessageBackgroundProperty, value)
End Set
End Property
<Category("Custom"), Description("Notification message pop up foreground brush.")> _
Public Property NotificationMessageForeground() As Brush
Get
Return CType(GetValue(NotificationMessageForegroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(NotificationMessageForegroundProperty, value)
End Set
End Property
<Category("Custom"), Description("Watermark text message. This is displayed if ...")> _
Public Property WatermarkMessage() As String
Get
Return CType(GetValue(WatermarkMessageProperty), String)
End Get
Set(ByVal value As String)
SetValue(WatermarkMessageProperty, value)
End Set
End Property
<Category("Custom"), Description("Watermark message pop up background brush.")> _
Public Property WatermarkMessageBackground() As Brush
Get
Return CType(GetValue(WatermarkMessageBackgroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(WatermarkMessageBackgroundProperty, value)
End Set
End Property
<Category("Custom"), Description("Watermark message pop up foreground brush.")> _
Public Property WatermarkMessageForeground() As Brush
Get
Return CType(GetValue(WatermarkMessageForegroundProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(WatermarkMessageForegroundProperty, value)
End Set
End Property
#End Region
#Region " Constructor and Initializer "
Shared Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(FormNotification), _
New FrameworkPropertyMetadata(GetType(FormNotification)))
End Sub
Private Sub FormNotification_Initialized(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles Me.Initialized
_objExpandedTimer = New System.Timers.Timer(AutoCollapseTimeout * 1000)
_objExpandedTimer.Enabled = False
_objExpandedTimer.AutoReset = False
End Sub
#End Region
#Region " Methods "
''' <summary>
''' When the expander collapses, need to remove the TextBlock adorner
''' </summary>
Private Sub _objErrorsExpander_Collapsed(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) _
Handles _objErrorsExpander.Collapsed
If _objErrorsExpanderAdornerLayer IsNot Nothing Then
_objErrorsExpanderAdornerLayer.Remove(_objTextBlockAdorner)
_objTextBlockAdorner = Nothing
_objErrorsExpanderAdornerLayer = Nothing
End If
End Sub
''' <summary>
''' When the expander expands, need to put the error message in the
''' adorner layer because the controls below it, may have their own
''' adorner validation error messages, this places the expander
''' popup on top of all other adorder layer elements.
''' </summary>
Private Sub _objErrorsExpander_Expanded(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) _
Handles _objErrorsExpander.Expanded
Dim objExpanderGrid As Grid = FindChilden.FindVisualChild(Of Grid)( _
_objErrorsExpander)
'
_objErrorsExpanderAdornerLayer = AdornerLayer.GetAdornerLayer( _
objExpanderGrid)
'
Dim txt As New TextBlock
txt.Text = Me.ErrorMessage
txt.Padding = New Thickness(5)
txt.Foreground = Me.ErrorPopUpForeground
txt.Background = Me.ErrorPopUpBackground
txt.BitmapEffect = New _
System.Windows.Media.Effects.DropShadowBitmapEffect
'
'need to move the TextBlock down below the expander and indent a little
Dim obj As New TranslateTransform(5, _objErrorsExpander.ActualHeight + 2)
txt.RenderTransform = obj
'
_objTextBlockAdorner = New TextBlockAdorner(objExpanderGrid, txt)
'
_objErrorsExpanderAdornerLayer.Add(_objTextBlockAdorner)
End Sub
Private Sub _objErrorsExpander_MouseEnter(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseEventArgs) _
Handles _objErrorsExpander.MouseEnter
_objExpandedTimer.Stop()
End Sub
Private Sub _objErrorsExpander_MouseLeave(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseEventArgs) _
Handles _objErrorsExpander.MouseLeave
If _objExpandedTimer.Interval > 0 Then
_objExpandedTimer.Start()
End If
End Sub
''' <summary>
''' Since WPF uses the STA threading model, another thread like a timer
''' can not update the UI. WPF provides a very simple technique for
''' updating the UI from another thread.
''' </summary>
Private Sub _objExpandedTimer_Elapsed(ByVal sender As Object, _
ByVal e As System.Timers.ElapsedEventArgs) _
Handles _objExpandedTimer.Elapsed
Dispatcher.Invoke(Windows.Threading.DispatcherPriority.Normal, New _
ExpanderDelegate(AddressOf CloseExpander))
End Sub
Private Sub _objTextBlockAdorner_MouseEnter(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseEventArgs) _
Handles _objTextBlockAdorner.MouseEnter
_objExpandedTimer.Stop()
End Sub
Private Sub _objTextBlockAdorner_MouseLeave(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseEventArgs) _
Handles _objTextBlockAdorner.MouseLeave
If _objExpandedTimer.Interval > 0 Then
_objExpandedTimer.Start()
End If
End Sub
''' <summary>
''' This method is called when the error message or notification message
''' property values are set and when the Time.Elapsed event fires.
''' This ensures that the expander region is closed and the adorner layer
''' is removed.
''' </summary>
''' <remarks></remarks>
Private Sub CloseExpander()
If _objErrorsExpander IsNot Nothing AndAlso _
_objErrorsExpander.IsExpanded Then
_objErrorsExpander.IsExpanded = False
End If
End Sub
''' <summary>
''' This is the call back that gets called when the ErrorMessage
''' dependency property is changed.
''' </summary>
Private Shared Sub OnErrorMessageChanged(ByVal d As DependencyObject, _
ByVal e As DependencyPropertyChangedEventArgs)
Dim obj As FormNotification = DirectCast(d, FormNotification)
If Not String.IsNullOrEmpty(e.NewValue.ToString) Then
obj.NotificationMessage = String.Empty
End If
obj.CloseExpander()
End Sub
''' <summary>
''' This is the call back that gets called when the NotificationMessage
''' dependency property is changed.
''' </summary>
Private Shared Sub OnNotificationMessageChanged(ByVal d As DependencyObject, _
ByVal e As DependencyPropertyChangedEventArgs)
DirectCast(d, FormNotification).CloseExpander()
End Sub
''' <summary>
''' This method is called by the WPF Dependency Property system when the
''' AutoCollapseTimeout value is set.
''' </summary>
Public Shared Function IsAutoCollapseTimeoutValid(ByVal value As Object) As Boolean
Dim dbl As Double = CType(value, Double)
If dbl < 0 OrElse dbl > 100 Then
Return False
Else
Return True
End If
End Function
''' <summary>
''' This is where you can get a reference to a control
''' inside the control template. Notice the PART_ naming convention.
''' </summary>
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
'
'Each object that you are getting a reference to here is also
'listed in a TemplatePart attribute on the class.
'
'<TemplatePart(Name:="PART_Expander", Type:=GetType(Expander))> _
'
_objErrorsExpander = CType(GetTemplateChild("PART_Expander"), Expander)
End Sub
#End Region
End Class
End Namespace
For the most part the Accessing Controls in the ControlTemplateThe If a control will be accessed in code, developers typically will declare a class level variable of the same
When designing your custom controls you must make a decision as to what happens if the Displaying the ErrorMessage in the Adorner LayerI have a blog post WPF Sample Series - Expander Control With Popup Content that explains how to make the If you look back at the I learned a lot about rendering text in the Adorner Layer from Josh Smith's blog post Rendering Text in the Adorner Layer and in fact am using his The The Next we need a reference to the Next the When the This is accomplish with a This is where we will use the We can now add the new Removing the ErrorMessage from the Adorner LayerAnytime the The Both the The
WPF uses the STA threading model. Other threads like the You should have a look at the System.ComponentModle.IDataErrorInfo Interface
If you want codeless form validation for your controls your business entity objects need to implement I have supplied a demo class called Customer. This class implements the In a future article I will show you how to implement declarative validation in your business entity objects that is all contained in a base class that any business entity object can derive from. The read-only The timing when the WPF 3.5 data binding system reads the For this series we will be performing validation when focus is lost. One reason for validating on Let's have a look at the XAML markup for the two <TextBox Width="100" Grid.Column="1" HorizontalAlignment="Left"
Margin="5,5,0,5" x:Name="txtFirstName"
Text="{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True}" ToolTip="Enter customers first name." />
<TextBox Width="100" Grid.Column="1" HorizontalAlignment="Left"
Margin="5,5,0,5" x:Name="txtLastName" Grid.Row="1"
Text="{Binding Path=LastName, Mode=TwoWay, UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True}" ToolTip="Enter customers last name." />
The binding statement is familiar to all WPF developers. What is new to WPF 3.5 is the Setting this property provides an alternative to using the DataErrorValidationRule element explicitly. The DataErrorValidationRule is a built-in validation rule that checks for errors that are raised by the IDataErrorInfo implementation of the source object. If an error is raised, the binding engine creates a ValidationError with the error and adds it to the Validation.Errors collection of the bound element. The lack of an error clears this validation feedback, unless another rule raises a validation issue. Bottom line, you just change your current binding statements by adding this new Customer Business ObjectAs I've stated before, the Customer class is coded so that we can demo the When the When the Public Class Customer
Implements INotifyPropertyChanged
Implements IDataErrorInfo
...
...
Default Public ReadOnly Property Item(ByVal strPropertyName As String) As String _
Implements System.ComponentModel.IDataErrorInfo.Item
Get
Return Me.CheckProperties(strPropertyName)
End Get
End Property
Public ReadOnly Property [Error]() As String _
Implements System.ComponentModel.IDataErrorInfo.Error
Get
Return Me.CheckProperties(String.Empty)
End Get
End Property
Private Function CheckProperties(ByVal strPropertyName As String) As String
Dim strResult As String = String.Empty
If String.IsNullOrEmpty(strPropertyName) OrElse _
String.Compare(strPropertyName, "FirstName", True) = 0 Then
If String.IsNullOrEmpty(_strFirstName) Then
strResult = "First Name is a required field."
End If
End If
If String.IsNullOrEmpty(strPropertyName) OrElse _
String.Compare(strPropertyName, "LastName", | ||||||||||||||||||||