Hi, Habr.
About a week ago I read the article
“How to get convenient access to XAML-resources from Code-Behind” and was not surprised. I apologize in
advance to EBCEu4 , the author of the aforementioned article, because I am going to criticize his approach a little.
I would like to note that the article contains only recommendations on the proper use of resources and does not pretend to be complete. My article will consist of three points. In the first, I will give an example of the situation when the above approach is justified, in the second - to try to explain why it is wrong to pull resources from the XAML markup in the code-behind, in the third - I will try to give an example of code that helps to avoid such actions.
Item 1. Lawyer
Let's take a closer look at the article I have indicated, and see what the matter is.
')
This material ended with a call to download the script and use it in their projects, but the author did not give himself the trouble to give an example of a situation when such an approach is justified (as was rightly noted in comments). In favor of this approach I can give only one example. Suppose for a moment that you are creating an application, one of the pages of which includes a list of users. You made a beautiful template for displaying the user, for example, like this: user photo (with the required size and scaling), name / nickname, Skype / phone number, and, of course, status - offline or online.
We will not see any problems on the computer - there is enough resources. But consider the situation when the list includes several thousand users, and you have a low-end device running WinPhone8 / 8.1 in your hands. Here, obviously, will begin problems with performance. ListView will be stupid when scrolling, artifacts will arise, and virtualization will not save. And if in the Universal App you can try to optimize performance using ContainerContentChanging, then in a Silverlight application this will not work (there is simply no such thing).
It is in such situations that such an approach is justified: you can refuse binding, spoiling the whole raspberry, and directly “feed” colors and other resources to controls / items in the sheet, etc. Looking ahead, when using MVVM and / or Dependency Injections, the game may not be worth the candle, which means we get bad-practice in our project, which can lead to a complication of validation of the final product in the store.
Item 2. The Prosecutor
And now -
to the wall closer to the point.
First, the approach itself surprised me. Why,
for the sake of all saints, except in the case given in the first paragraph, pull resources from the “native” environment for them (XAML markup) in the code-behind and get an extra chance to get confused in them? I personally find this practice just criminal and perverted. And I am going to “poke a little” with my finger in places of this approach that are weak in my opinion.
So:
If there is a sudden need to pull resources into the code-behind, it is, gentlemen and ladies, a crutch, which is a sign of either poor architecture, or poorly written controls / resources, or both.
Imagine a situation (even for a moment), that your project shot
you in the leg and you get income. But a year goes by and the guys from Microsoft at the
annual event promise the introduction of a new ecosystem or at least a change in the existing one. What do we get? That's right, a high probability of crash as a result of an attempt to draw a renamed, for example, system color or a margin property (or at least anything, in principle). Accordingly, it is necessary to climb again into the forgotten code and
pick up a file to redo a whole bunch. Not a good prospect, is it?
The problem with the development. If you work hard on something resembling Enterprise, then most likely you use the MVVM pattern and Dependency Injections. In this case, it is still more fun, because with MVVM you will have to first pull the resource into your VM, and only then forget it to control the view: there is no profit in the performance, and the crutch is here, my dear! With DI, porridge is brewed even cooler. Suppose you, in no case, have made your service from the XAML parser, and pull it on an unlimited number of VMs. Here the parser itself will put a pig on you, for xpath is not the fastest thing, unfortunately. And if you have a lot of calls to this service, you'd better not write it at all. From myself I will say that if I tried to use a similar approach on the current project (except for the situation from the example), I would get it.
The problem with testing. In the Enterprise case mentioned, 100% of the tests will be written or, even better, you will use TDD during development. How do you want to cover such a parser with tests?
Point 3. And what to do?
Well, who criticizes - is obliged to provide an alternative solution. I offer you my own way, which allows you to fully control the presentation of your product without extra crutches and bicycles. Yes, the code will be more, but the headache is less. Meet:
Converters
For example, depending on the status of the user (offline / online) received from the server, you need to change the corresponding text in the list item.
Code:
public class UserStateToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value != null && value is UserState) { switch ((UserState)value) { case UserState.Online: return StringResources.Online; case UserState.Offline: return StringResources.Offline; } } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new System.NotImplementedException(); } }
ConvertBack is not implemented simply as superfluous.
UserState is our enum for 2 values (Online / Offline), and StringResources.Online/Offline - the corresponding localized resources (strings).
The use of such a converter:
Register the namespace with converters:
xmlns:converters="clr-namespace:MyApp.Converters"
We register our converter for user state:
<converters:UserStateToStringConverter x:Key="UserStateToStringConverter"/>
Create a template for the user list item.
<DataTemplate x:Key="AudioIconTemplate"> ... Header="{Binding UserState, Converter={StaticResource UserStateToStringConverter}}" ... - , . </DataTemplate>
Here you are. Now we see the state in which this or that user is in our list. Just like a string, the converter can return a property of type Visibility or something else, up to a DataTemplate (although there is another treat for templates). Go ahead to the XAML resources themselves.
Control states - Control States.
With the help of states, you can do everything you want with control with just a few lines of code! Suppose that while the user is active, the item in the list has a light background, and when it is inactive it becomes darker. To achieve this, the list element should be control - here the DataTemplate will be replaced with ControlTemplate - and all its (control) states should be described in the ControlTemplate.
For example:
<ControlTemplate TargetType="controls:UserControl"> <Grid x:Name="Container" Background="{TemplateBinding Background}"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="UserStates"> <VisualState x:Name="OfflineState"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="UserStateTextBlock"> <EasingColorKeyFrame KeyTime="0" Value="{Binding DoneBadColor, RelativeSource={RelativeSource TemplatedParent}}"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="OnlineState"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="UserStateTextBlock"> <EasingColorKeyFrame KeyTime="0" Value="{Binding DoneAverageColor, RelativeSource={RelativeSource TemplatedParent}}"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateManager.VisualStateGroups> ...... , .
Total, the layout is ready. Now let's see depending on what and how we will change our states. We have to make a property for our control, to which we will bind the state of the user, obtained from the server in our VM, and from which we push off further:
public static readonly DependencyProperty UserStateProperty = DependencyProperty.Register("IndentAngle", typeof(UserState), typeof(UserControl), new PropertyMetadata(OnUserStatePropertyChanged));
As you can see, we want the change of the UserStateProperty property to call the OnUserStatePropertyChanged method. It might look like this:
public static void OnProgressControlPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { UserControl sender = d as UserControl; if(sender != null) { sender.ChangeVisualState((UserState)e.NewValue); } }
The ChangeVisualState method will look like this:
private void ChangeVisualState(UserState newState) { switch (newState) { case UserState.Offline: VisualStateManager.GoToState(this, "OfflineState", false); break; case UserState.Online: VisualStateManager.GoToState(this, "OnlineState", false); break; } }
Now our control will calmly change its color depending on the value obtained.
Sweet - for dessert.
In addition to such trivial things as converters and states, there is also such a thing as ContentControl. And so it allows you to do a lot. Although, in principle, the approach is brazenly pulled at the converter, but it has been significantly expanded by the nature of ContentControl itself. Take a look:
The base class for DataTemplateSelector (after all, there can be many concrete implementations, isn't it?):
public abstract class DataTemplateSelector : ContentControl { protected abstract DataTemplate GetTemplate(object item, DependencyObject container); protected override void OnContentChanged(object oldValue, object newValue) { base.OnContentChanged(oldValue, newValue); ContentTemplate = GetTemplate(newValue, this); } }
And a specific example for our user:
public class UserStateTemplateControl : DataTemplateSelector { public DataTemplate UserOnlineTemplate { get; set; } public DataTemplate UserOfflineTemplate { get; set; } protected override DataTemplate GetTemplate(object item, DependencyObject container) { UserState state = UserState.Offline; if (item != null && item is UserState) { state = (UserState)item; } switch (state) { case CategoryProgressStatus.Offline: return UserOnlineTemplate; case CategoryProgressStatus.Online: return UserOfflineTemplate; } return null; } }
Ta-a-ak, the class is ready for their needs. Now let's play with XAML.
Let's declare the namespace with our control - selector.
xmlns:controls="clr-namespace:MyApp.Controls;assembly=MyApp.Controls"
Let's write our additional templates that will respond to different states of the control:
<DataTemplate x:Key="UserOfflineStateTemplate"> <TextBlock Text="Offline" Foreground="{StaticResource StatusOnlineBrush}"/> </DataTemplate> <DataTemplate x:Key="UserOnlineStateTemplate"> <TextBlock Text="Online" Foreground="{StaticResource StatusOfflineBrush}"/> </DataTemplate>
Where StatusOfflineBrush and StatusOnlineBrush are abstract brushes that we, if necessary, initialized bi higher in the XAML markup.
And we will slightly change the DataTemplate for the user from step number 1:
<DataTemplate x:Key="UserTemplate"> ... <controls:StatProgressTemplateControl Content="{Binding UserState}" OfflineTemplate="{StaticResource NotStartedIconTemplate}" OnlineTemplate="{StaticResource UserOnlineStateTemplate}"/> ... . </DataTemplate>
Well, waiting for the
thunder and lightning constructive criticism on his head.
Thank you all in advance.