動機
UWP(Universal Windows Platform)でアプリを作っています。この中で「テキストボックスに入力した値が条件を満たしていない場合はエラーだと分かるように表示して欲しい」っていう要求をいただきまして。MVVMデザインパターンで設計・実装している当方としてはコードビハインドにStyle関係のものをごちゃごちゃ書くのはイヤだったので(せめてViewModel内でプロパティを変更したらStyleが変更されるようにしたい)、少し考え込んでしまったわけです。
やったこと
試行錯誤したあげく、結局App.csでリソースを持っておいて、各ViewModelからはApp.csで書いたStyleのリソース名をプロパティで持っておき、それに変更が掛かったときConverter経由でApp.csのリソースを割り当てる…という方式が一番素直かなって気がしてきたので(エラー状態の表示は各ページで共通化されていた方が直感的で分かりやすそうですし…)それで実装しています。
app.xamlでResourceを読み込んでおいて…
<Application
x:Class="Application.App"
xmlns:views="using:Application.Views"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Application">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/ResourceDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
読み込んだリソース(Views/ResourceDictionary.xaml)には以下のように各テキストボックスに対してのスタイルを決めておきます…。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="TextBox" x:Key="InputNormal">
<Setter Property="FocusVisualPrimaryBrush" Value="LightBlue" />
<Setter Property="FocusVisualSecondaryBrush" Value="AliceBlue" />
<Setter Property="FocusVisualPrimaryThickness" Value="1" />
<Setter Property="FocusVisualSecondaryThickness" Value="1" />
<Setter Property="BorderBrush" Value="Blue" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Background" Value="FloralWhite" />
</Style>
<Style TargetType="TextBox" x:Key="InputError">
<Setter Property="FocusVisualPrimaryBrush" Value="DeepPink" />
<Setter Property="FocusVisualSecondaryBrush" Value="LightPink" />
<Setter Property="FocusVisualPrimaryThickness" Value="1" />
<Setter Property="FocusVisualSecondaryThickness" Value="1" />
<Setter Property="SelectionHighlightColor" Value="HotPink" />
<Setter Property="SelectionHighlightColorWhenNotFocused" Value="HotPink" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="MediumVioletRed" />
<Setter Property="Background" Value="LightPink" />
</Style>
</ResourceDictionary>
各ページではスタイル名のプロパティに対してConverterを指定したものをTextBoxのStyleにデータバインディングします…(Statusにはリソース名文字列が入るようにしました)。
<Page
xmlns:helpers="using:Application.Helpers"
>
<Page.Resources>
<helpers:StatusToStyleConverter x:Key="statusToStyle" />
</Page.Resources>
<Grid>
...
<TextBox x:Name="VAL"
x:Uid="Val"
Style="{Binding Status, Converter={StaticResource statusToStyle}}" />
...
</Grid>
</Page>
そして、StatusToStyleConverterクラスにて文字列とStyleに関するリソースとを変換させるようにしました。App.xamlに登録したリソースはApplication.App.Current.Resourcesでどこからでも見えるのですが、それをConverterに押し込んだ格好です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
namespace Application.Helpers
{
public class StatusToStyleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
Style style = null;
string _stylename = value as string;
if (Application.App.Current.Resources.ContainsKey(_stylename))
{
style = Application.App.Current.Resources[_stylename] as Style;
}
return style;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
string name;
try
{
Style style = value as Style;
KeyValuePair<object, object> resource = Application.App.Current.Resources.FirstOrDefault(res => res.Value.Equals(style));
name = resource.Key as string;
}
catch (ArgumentNullException)
{
name = null;
}
return name;
}
}
}
まだ不満ですけども…
今思いつく限り上記のやり方を取れば、ViewModelがUI関連のクラス(Windows.UI.Xamlほにゃらら)で汚されることはないのでまぁ良いんですが、正直言うとまだ少し不満が残っています…もっとうまいやり方はあるものなのでしょうか?
まぁ、どのみちページ単位で指定したリソースを動的に切り替えたいとか言い出されるとViewModelにWindows.UI.Xaml.関連のクラスを渡さないわけにいかなくなりそうなので、これが精一杯かなと諦めてもいますが…。