
In 2006, WPF and Silverlight software platforms were provided to developers along with .NET 3.0. Over the next ten years, Microsoft has released new versions of its operating system and their respective platforms. And so, in 2016, the Universal Windows Platform was released along with Windows 10.
All platforms differed to some extent with the capabilities of the API, but the XAML markup language, which was common to all of them, remained virtually unchanged. Therefore, all developers, regardless of the platform on which they work, are faced with the same tasks: expanding or modifying existing ones, as well as developing new controls. These are very important skills needed to develop applications that meet the requirements of design and functionality.
')
These tasks are due to the fact that on any platform the developer has a limited set of controls necessary for developing applications. Its toolkit consists of elements from the Microsoft distribution (in the case of the UWP - Windows Universal Platform SDK) and from third-party vendors or developers. Even collectively, they cannot cover all the requirements that arise when developing applications. Existing controls may not be suitable for a number of reasons: appearance, behavior or functioning. Unfortunately, to this day there is no single source of information that would provide detailed and accessible information about the solutions to these problems. All that remains for developers for a long time is to collect information on the Internet, bit by bit.
The purpose of this series of three articles is to systematize ways of changing, expanding and creating new controls.
Part 1. Extending Existing ControlsThe first part will discuss the expansion of existing controls without interfering with their internal structure.
Suppose that the general behavior and operation of the control suits the developer, but it needs to be expanded. For example, the
TextBox control provides the ability to enter data, but is devoid of validation functionality. The easiest way to get the desired result is to add logic to the code-behind of the view (View) containing this
TextBox .
public sealed partial class MainPage : Page { public MainPage () { this.InitializeComponent (); textbox.TextChanged += Textbox_TextChanged; } private void Textbox_TextChanged (object sender, TextChangedEventArgs e) {
However, the development of UWP involves the use of the MVVM architectural pattern, one of the main goals of which is to separate logic from presentation. Therefore, it should be encapsulated either in the ViewModel of the view, or in the new control, interaction with which it will be performed as with a black box without violating the principles of MVVM.
Thus, the solution presented in the listing above is only suitable in the case of great developer laziness, confident that at subsequent stages of development it will not bother him. In the case, if similar validation is required in more than one place within the application, this is a direct indication that it is necessary to take this logic into a separate entity.
There are two ways to expand controls without interfering with their internal structure and functioning, the implementation of which can be compared with parasitism - attached properties and behaviors.
Attached PropertiesAttached property is a type of dependency property defined in a separate class and attached to the target object at the XAML level.
Consider the mechanism of the attached properties on the above example of validation
TextBox for the registration page.
Invalid and valid registration formsDefine the
TextBoxExtensions class that contains the following attached properties:
1.
RegexPattern - a property that accepts a RegEx validation template string as input. In case the line is empty, we assume that validation of the input field is not required.
2.
IsValid is a property containing the value of the current validation status of the input field based on the template specified in the
RegexPattern property.
This class also contains the
OnRegexPatternChanged method, which is triggered when the value of the
RegexPattern property
changes . If its value is not empty, then we subscribe to the
TextChanged event of the TextBox control, in the context of which the
RegexPattern and
IsValid properties
operate .
In the
Textbox_TextChanged event handler,
we call the
ValidateText method, validating the string according to the passed pattern. We assign its result to the
IsValid property.
public class TextBoxExtensions { public static string GetRegexPattern (DependencyObject obj) { return (string) obj.GetValue (RegexPatternProperty); } public static void SetRegexPattern (DependencyObject obj, string value) { obj.SetValue (RegexPatternProperty, value); } public static readonly DependencyProperty RegexPatternProperty = DependencyProperty.RegisterAttached ("RegexPattern", typeof (string), typeof (TextBoxExtensions), new PropertyMetadata (string.Empty, OnRegexPatternChanged)); public static bool GetIsValid (DependencyObject obj) { return (bool) obj.GetValue (IsValidProperty); } public static void SetIsValid (DependencyObject obj, bool value) { obj.SetValue (IsValidProperty, value); } public static readonly DependencyProperty IsValidProperty = DependencyProperty.RegisterAttached ("IsValid", typeof (bool), typeof (TextBoxExtensions), new PropertyMetadata (true)); private static void OnRegexPatternChanged (DependencyObject d, DependencyPropertyChangedEventArgs e) { var textbox = d as TextBox; if (textbox == null) { return; } textbox.TextChanged -= Textbox_TextChanged; var regexPattern = (string) e.NewValue; if (string.IsNullOrEmpty (regexPattern)) { return; } textbox.TextChanged += Textbox_TextChanged; SetIsValid (textbox, ValidateText (textbox.Text, regexPattern)); } private static void Textbox_TextChanged (object sender, TextChangedEventArgs e) { var textbox = sender as TextBox; if (textbox == null) { return; } if (ValidateText (textbox.Text, GetRegexPattern (textbox))) { SetIsValid (textbox, true); } else { SetIsValid (textbox, false); } } private static bool ValidateText (string text, string regexPattern) { if (Regex.IsMatch (text, regexPattern)) { return true; } return false; } }
Next we bind these properties to the input fields in the markup and set the values of the
RegexPattern property.
<TextBox Grid.Row="1" Grid.Column="1" ap:TextBoxExtensions.RegexPattern="." ap:TextBoxExtensions.IsValid="{x:Bind ViewModel.IsUserNameValid, Mode=TwoWay}" IsSpellCheckEnabled="False"/> <TextBox Grid.Row="2" Grid.Column="1" ap:TextBoxExtensions.RegexPattern="^\d{2}\.\d{2}\.\d{4}$" ap:TextBoxExtensions.IsValid="{x:Bind ViewModel.IsBirthdateValid, Mode=TwoWay}"/> <TextBox Grid.Row="3" Grid.Column="1" ap:TextBoxExtensions.RegexPattern="^([\w\.\-]+)@([\w\-]+)((\.(\w){2,4})+)$" ap:TextBoxExtensions.IsValid="{x:Bind ViewModel.IsEmailValid, Mode=TwoWay}" IsSpellCheckEnabled="False"/> <PasswordBox Grid.Row="4" Grid.Column="1" ap:PasswordBoxExtensions.RegexPattern="." ap:PasswordBoxExtensions.IsValid="{x:Bind ViewModel.IsPasswordValid, Mode=TwoWay}" />
We have a clean code-behind.
public sealed partial class RegistrationView : UserControl { public RegistrationViewModel ViewModel { get; private set; } public RegistrationView () { this.InitializeComponent (); this.DataContext = ViewModel = new RegistrationViewModel (); } }
And the logic of the availability of the registration button at the ViewModel level.
public class RegistrationViewModel : BindableBase { private bool isUserNameValid = false; public bool IsUserNameValid { get { return isUserNameValid; } set { Set (ref isUserNameValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } private bool isBirthdateValid = false; public bool IsBirthdateValid { get { return isBirthdateValid; } set { Set (ref isBirthdateValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } private bool isEmailValid = false; public bool IsEmailValid { get { return isEmailValid; } set { Set (ref isEmailValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } private bool isPasswordValid = false; public bool IsPasswordValid { get { return isPasswordValid; } set { Set (ref isPasswordValid, value); RaisePropertyChanged (nameof (IsRegisterButtonEnabled)); } } public bool IsRegisterButtonEnabled { get { return IsUserNameValid && IsBirthdateValid && IsEmailValid && IsPasswordValid; } } }
Listing of
PasswordBoxExtensions class is omitted, because repeats the
TextBoxExtensions class a little less than completely and exists only for the reason that both controls are not inherited from some abstract
TextInput class from which they could receive common fields and events, but from the too general
Control class.
Thanks to the attached properties, we managed to expand the functionality of the existing
TextBox and
PasswordBox classes without interfering with their internal structure. And we did not even need to generate a new descendant class from them, which is not always possible.
Behaviors (Behaviors)Behaviors appeared in Expression Blend 3 in order to provide developers with a mechanism for solving such tasks arising on the user interface side, such as: animations, visual effects, drag-and-drop, etc.
UWP does not provide a library for working with behaviors. Being part of the Expression Blend SDK, it must be installed separately, for example, via
Nuget .
Suppose that we are working with a
FlipView control and it is required that when it is scrolled through, a new element plays the appearance animation.
Behavior animationWe define the class
FlipViewItemFadeInBehavior , inherited from the class
BehaviorT , where
T is the name of the class to which or whose descendants you can add the desired behavior.
In it, we override the
OnAttached method, in which we subscribe to the
SelectionChanged event of an associated object of type
FlipView .
In the event handler
FlipView_SelectionChanged, we attach the required animation to the new element and launch it. The animation playback time can be parameterized by defining the
Duration property.
public class FlipViewItemFadeInBehavior : Behavior<FlipView> { public double Duration { get; set; } protected override void OnAttached () { base.OnAttached (); AssociatedObject.SelectionChanged += FlipView_SelectionChanged; } protected override void OnDetaching () { base.OnDetaching (); AssociatedObject.SelectionChanged -= FlipView_SelectionChanged; } private void FlipView_SelectionChanged (object sender, SelectionChangedEventArgs e) { var flipView = sender as FlipView; var selectedItem = flipView.SelectedItem as UIElement; Storyboard sb = new Storyboard (); DoubleAnimation da = new DoubleAnimation { Duration = new Duration (TimeSpan.FromSeconds (Duration)), From = 0d, To = 1d }; Storyboard.SetTargetProperty (da, "(UIElement.Opacity)"); Storyboard.SetTarget (da, selectedItem); sb.Children.Add (da); sb.Begin (); } }
Now we are ready to add this behavior to the required controls in the markup.
xmlns:b="using:ArticleSandbox.Controls.Behaviors" xmlns:i="using:Microsoft.Xaml.Interactivity" <FlipView HorizontalAlignment="Center" VerticalAlignment="Center"> <FlipView.Items> <Rectangle Fill="Red" Width="200" Height="100"/> <Rectangle Fill="Green" Width="200" Height="100"/> <Rectangle Fill="Blue" Width="200" Height="100"/> </FlipView.Items> <i:Interaction.Behaviors> <b:FlipViewItemFadeInBehavior Duration="2"/> </i:Interaction.Behaviors> </FlipView>
Thus, we managed to bring the logic of animation into a separate entity with the subsequent possibility of using it through the definition in the markup.
Both mechanisms examined expand the existing controls and it is important to clearly understand when and which mechanism to use.
If it is necessary to extend the control of some kind of logic, then this is a sign that the result can be achieved by means of attached properties. If the control needs to provide some kind of visual effect or animation, then you should pay attention to the behavior mechanism.
Continued in the second part: "
Modifying Existing Controls "
Yang Moroz, Senior .NET Developer