Out team wants to create reusable, stylable Views. For example, we want to reuse in different apps the CommonPromptView
(our own, customizable dialog box where we can hide "Cancel" button, set the header, show specific icon and so on).
That view has several elements on it's surface: TextBlocks, Buttons. We want to make them stylable to.
Well, what is the best approach to solve this task?
- The view can be of type Window.
- The view can be of type UserControl.
In the first case, styling can be supported in two ways:
- Styles of elements have references to DynamicResources.
- Styles are passed in to the constructor of a View.
The both are not clean (in my opinion).
But if a View is a UserControl, then each time somebody who creates new app has to create a new Window to contain a UserControl and set it's bindings to DPs (of Style type) properly. Besides, if a UserControl has it's own very convinient API (static methods for the most frequently used operations) which will be lost for the user of the Window which contains the UserControl.
Update
Example of CommonPromptView
implemented as a UserControl.
Code-behind
public sealed partial class CommonPromptView {
private const int CloseViewTimeIntervalInMilliseconds = 120000;
private DispatcherTimer timer;
public static readonly DependencyProperty CommonPromptBorderStyleProperty = DependencyProperty.Register(
"CommonPromptBorderStyle", typeof (Style), typeof (CommonPromptView), new PropertyMetadata(default(Style)));
public Style CommonPromptBorderStyle {
get { return (Style) GetValue(CommonPromptBorderStyleProperty); }
set { SetValue(CommonPromptBorderStyleProperty, value); }
}
public static readonly DependencyProperty CommonPromptHeaderStyleProperty = DependencyProperty.Register(
"CommonPromptHeaderStyle", typeof (Style), typeof (CommonPromptView), new PropertyMetadata(default(Style)));
public Style CommonPromptHeaderStyle {
get { return (Style) GetValue(CommonPromptHeaderStyleProperty); }
set { SetValue(CommonPromptHeaderStyleProperty, value); }
}
public static readonly DependencyProperty CommonPromptMessageStyleProperty = DependencyProperty.Register(
"CommonPromptMessageStyle", typeof (Style), typeof (CommonPromptView), new PropertyMetadata(default(Style)));
public Style CommonPromptMessageStyle {
get { return (Style) GetValue(CommonPromptMessageStyleProperty); }
set { SetValue(CommonPromptMessageStyleProperty, value); }
}
public static readonly DependencyProperty CommonPromptSpitterStyleProperty = DependencyProperty.Register(
"CommonPromptSpitterStyle", typeof (Style), typeof (CommonPromptView), new PropertyMetadata(default(Style)));
public Style CommonPromptSpitterStyle {
get { return (Style) GetValue(CommonPromptSpitterStyleProperty); }
set { SetValue(CommonPromptSpitterStyleProperty, value); }
}
public static readonly DependencyProperty CommonPromptButtonStyleProperty = DependencyProperty.Register(
"CommonPromptButtonStyle", typeof (Style), typeof (CommonPromptView), new PropertyMetadata(default(Style)));
public Style CommonPromptButtonStyle {
get { return (Style) GetValue(CommonPromptButtonStyleProperty); }
set { SetValue(CommonPromptButtonStyleProperty, value); }
}
[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public CommonPromptView(object header = null, string message = "Вы действительно хотите продолжить?",
Visibility headerVisibility = Visibility.Collapsed,
MessageBoxIconWPF iconType = MessageBoxIconWPF.Question,
object affirmButtonContent = null, object cancelButtonContent = null,
Visibility cancelButtonVisibility = Visibility.Visible, Visibility affirmButtonVisibility = Visibility.Visible) {
InitializeComponent();
Header.Content = header ?? string.Empty;
if (header == null)
HeaderSplitter.Visibility = Visibility.Collapsed;
Message.Content = message;
Ok.Content = affirmButtonContent ?? "ОК";
Cancel.Content = cancelButtonContent ?? "Отмена";
Cancel.Visibility = cancelButtonVisibility;
Header.Visibility = headerVisibility;
Ok.Visibility = affirmButtonVisibility;
BitmapImage icon = new BitmapImage();
icon.BeginInit();
icon.UriSource = new Uri(GetIconPath(iconType));
//new Uri("pack://application:,,,/ApplicationName;component/Resources/logo.png");
icon.EndInit();
Icon.Source = icon;
SetTimer();
}
private static string GetIconPath(MessageBoxIconWPF icon) {
const string uri = "pack://application:,,,/Microtech.WPF.Common;component/";
string iconName;
switch (icon) {
case MessageBoxIconWPF.Error:
iconName = "CustomDialogStop";
break;
case MessageBoxIconWPF.Information:
iconName = "CustomDialogInformation";
break;
case MessageBoxIconWPF.Question:
iconName = "CustomDialogQuestion";
break;
case MessageBoxIconWPF.Warning:
iconName = "CustomDialogWarning";
break;
default:
throw new ArgumentException("There were no such an image");
}
return uri + string.Format("Images/{0}.{1}", iconName, "png");
}
public CommonPromptView(string content, Visibility cancelButtonVisibility = Visibility.Visible)
: this(message: content, cancelButtonVisibility: cancelButtonVisibility) {
}
private void SetTimer() {
TimeSpan timerInterval = TimeSpan.FromMilliseconds(CloseViewTimeIntervalInMilliseconds);
timer = new DispatcherTimer(timerInterval, DispatcherPriority.ApplicationIdle,
(obj, e) => Cancel_Click(null, null),
Dispatcher.CurrentDispatcher);
timer.Start();
}
public CommonPromptView() {
InitializeComponent();
}
public static bool PromptOnUserAgreement(string header, string message, string okText = "Да",
string cancelText = "Нет") {
return new CommonPromptView(header,
message, Visibility.Visible, MessageBoxIconWPF.Information, okText, cancelText).ShowDialog()
.GetValueOrDefault();
}
public static void PromptOnWarning(string header, string message) {
new CommonPromptView(header, message, headerVisibility: Visibility.Visible, iconType: MessageBoxIconWPF.Warning,
cancelButtonVisibility: Visibility.Collapsed).ShowDialog();
}
public static void PromptOnError(string header, string message) {
new CommonPromptView(header, message, headerVisibility: Visibility.Visible, iconType: MessageBoxIconWPF.Error,
cancelButtonVisibility: Visibility.Collapsed).ShowDialog();
}
public static void PromptOnSuccess(string header, string message) {
new CommonPromptView(header, message, headerVisibility: Visibility.Visible,
iconType: MessageBoxIconWPF.Information,
cancelButtonVisibility: Visibility.Collapsed).ShowDialog();
}
private void Ok_Click(object sender, RoutedEventArgs e) {
StopTimer();
TryCloseTheWindow(true);
}
private void Cancel_Click(object sender, RoutedEventArgs e) {
StopTimer();
TryCloseTheWindow(false);
}
private void TryCloseTheWindow(bool dialogResult) {
Window parentwin = GetWindow(this);
if (parentwin != null) {
try {
parentwin.DialogResult = dialogResult;
} catch (InvalidOperationException) {
}
parentwin.Close();
}
}
private void StopTimer() {
if (timer != null) {
timer.Stop();
timer = null;
}
}
}
XAML
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Microtech.WPF.Common.CommonPromptView"
Background="Transparent"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<Style x:Key="DefaultViewStyle" TargetType="Window">
<Setter Property="ResizeMode" Value="NoResize" />
<Setter Property="ShowInTaskbar" Value="False" />
<Setter Property="WindowStyle" Value="None" />
<Setter Property="WindowState" Value="Normal" />
<Setter Property="SizeToContent" Value="WidthAndHeight" />
<Setter Property="Topmost" Value="True" />
<Setter Property="Cursor" Value="Arrow" />
</Style>
<Style x:Key="DefaultHeaderStyle" TargetType="Label">
<Setter Property="Margin" Value="10,5,5,5"/>
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="20" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style x:Key="DefaultBorderStyle" TargetType="Border">
<Setter Property="Background" Value="#ADAAAD"/>
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Padding" Value="10" />
<Setter Property="BorderThickness" Value="3"/>
<Setter Property="CornerRadius" Value="10"/>
</Style>
<Style x:Key="DefaultMessageStyle" TargetType="Label">
<Setter Property="Margin" Value="10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontFamily" Value="Verdana"/>
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style x:Key="DefaultSplitterStyle" TargetType="GridSplitter">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="BorderThickness" Value="0.65" />
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="IsEnabled" Value="False" />
</Style>
<Style x:Key="FStandartButton" TargetType="{x:Type Button}">
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="BorderBrush" Value="#00000000" />
<Setter Property="Foreground" Value="White" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Height" Value="Auto" />
<Setter Property="MinHeight" Value="55" />
<Setter Property="Width" Value="420" />
<Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="10" />
<Setter Property="FontSize" Value="22" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" BorderBrush="#FF000000" BorderThickness="1,1,1,1" CornerRadius="4,4,4,4">
<Border.Background>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0" Color="#8C8A8C" />
<GradientStop Offset="1" Color="#636163" />
</LinearGradientBrush>
</Border.Background>
<ContentPresenter Name="ContentContainer"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#CC000000" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DefaultButtonStyle" TargetType="Button" BasedOn="{StaticResource FStandartButton}">
<Setter Property="FontFamily" Value="Verdana"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="6"/>
</Style>
</UserControl.Resources>
<Border Style="{Binding CommonPromptBorderStyle, TargetNullValue={StaticResource DefaultBorderStyle}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label x:Name="Header"
Style="{Binding CommonPromptHeaderStyle, TargetNullValue={StaticResource DefaultHeaderStyle}}" />
<GridSplitter x:Name="HeaderSplitter" Grid.Row="0"
Style="{Binding CommonPromptSpitterStyle, TargetNullValue={StaticResource DefaultSplitterStyle}}"
/>
<StackPanel Grid.Row="1" Margin="5,10,5,0" Orientation="Horizontal">
<Image x:Name="Icon" Width="32" Height="32" Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
<Label x:Name="Message"
Style="{Binding CommonPromptMessageStyle,
TargetNullValue={StaticResource DefaultMessageStyle}}" />
</StackPanel>
<StackPanel Grid.Row="2" HorizontalAlignment="Center" Orientation="Horizontal">
<Button x:Name="Ok"
Style="{Binding CommonPromptButtonStyle, TargetNullValue={StaticResource DefaultButtonStyle}}"
Click="Ok_Click" />
<Button x:Name="Cancel"
Style="{Binding CommonPromptButtonStyle, TargetNullValue={StaticResource DefaultButtonStyle}}"
IsDefault="True" Click="Cancel_Click" />
</StackPanel>
</Grid>
</Border>
I think in your case you need to look in the direction of
DataTemplate
, to make the dynamic content. I made a few examples that show this. The general meaning of this examples:Clearly, this is not the most realistic example, but it's just a way to show the dynamic selection of content. For you, you will determine the condition of replacement content.
Example A
This example demonstrates the dynamic replacing
DataTemplates
, depending on the input values. If we think in terms of style patterns, it is very similar to the abstract factory, where instead of classes -DataTemplate
, and factory method is a dynamicDataTemplate
selector. Example is fully suitable forMVVM
pattern. Below is an example:MainWindow.xaml
MainView.xaml
It's a
DataTemplate
inResourceDictionary
. There are two templates: UserTemplate and AdminTemplate. One for the user and one for the administrator. In the style ofContentControl
defined ContentTemplateSelector, and a collection of templates that will be installed by the condition. In propertyContent
for DynamicContentControl is set content string that can be:User
orAdmin
.MainViewModel.cs
Here in commands set the content type.
MainModel.cs
DynamicTemplateSelector
Taken and little reworked from CodeProject:
Result for User
Result for Admin
Notes about this way
Advantages:
Disadvantages:
Conclusion:
This method is suitable for completely different views, if the representations are not much different, you see a second example.
Example B
This example uses a one
DataTemplate
, the data is taken from the model, by default, all controls are hidden (Visibility.Collapsed
), all actions inView
are performed viaDataTriggers
. Example is fully suitable forMVVM
pattern.MainWindow.xaml
MainView.xaml
MainViewModel.cs
MainModel.cs
Result for User
Result for Admin
Notes about this way
Advantages:
Disadvantages:
View
the need to manipulate a lot of controls and data.Conclusion:
This method is suitable for
Views
, which is not very different from each other, and the number of different values is not too large.