かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ユニーバサルWindowsアプリのPrism for Windows RuntimeでViewのマッピングとViewModelのマッピングのカスタマイズ方法

Prism for Windows Runtimeでは、標準でViewの型はViews名前空間におかないといけないとか、ViewModelはViewModels名前空間に{Viewの型名}ViewModelで置かないといけないとかルールを持っています。

実はこのルールは別のものに置き換えることが可能です。ということで置き換える方法のサンプルコードをコードレシピに書きました。

FlipViewの上下移動のボタンをクリックしたときにTappedイベントが発生してしまうのをどうにかしたい

表題の通り、FlipViewのTappedイベントで画面遷移を行う処理を書いていたら、マウスのときにFlipViewに表示される上下移動のためのボタンをクリックしただけで画面遷移するようになって悩んでました。

同じ問題に悩んでる人が海外にもいたみたいで話題になってたのを見て解決しました。

上記ページのままですが、仕掛けとしては、FlipViewのVisualTree上にあるボタンをとってきてTappedイベントを仕掛けてe.Handled = trueしてやることでFlipViewまでイベントを上げないようにするという感じです。

コードにすると、こんなユーテリティを用意します。

public static class VisualTreeHelperExtensions
{
    public static IEnumerable<T> GetDescendantsOfType<T>(FrameworkElement target)
        where T : DependencyObject
    {
        return GetDescendants(target).OfType<T>();
    }

    public static IEnumerable<DependencyObject> GetDescendants(DependencyObject target)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(target); i++)
        {
            var r = VisualTreeHelper.GetChild(target, i);
            yield return r;
            foreach (var c in GetDescendants(r))
            {
                yield return c;
            }
        }
    }
}

そして、FlipViewのLoadedイベントで以下のようなコードを書きます。

private void FlipView_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    var flipView = (FlipView)sender;
    var buttons = VisualTreeHelperExtensions.GetDescendantsOfType<Button>(flipView);
    foreach (var button in buttons)
    {
        button.Tapped += (_, args) => args.Handled = true;
    }
}

Behaviorにまとめてしまってもいいかもしれませんね。

HubSectionのヘッダーのグリフを消したい

Hubコントロールは便利なのですが、HubSectionのヘッダーに何か操作をするコントロールを置こうとすると反応してくれません。 IsHeaderInteractiveをTrueにすると反応するようになるのですが、今度はマウスオーバーとかで色がついたり>みたいなグリフがついたりして邪魔です。そんなときは、こんなStyleをHubSectionに適用すれば解決します。

HubSectionのテンプレートから、いらない部分を除去した感じです。

<Style x:Key="NoGlyphHubSectionStyle" TargetType="HubSection">
    <Setter Property="HorizontalAlignment" Value="Left"/>
    <Setter Property="VerticalAlignment" Value="Stretch"/>
    <Setter Property="HorizontalContentAlignment" Value="Left"/>
    <Setter Property="VerticalContentAlignment" Value="Top"/>
    <Setter Property="Padding" Value="40,40,40,44"/>
    <Setter Property="IsTabStop" Value="False"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="HubSection">
                <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
                    <Border.Resources>
                        <ControlTemplate x:Key="HeaderButtonTemplate" TargetType="Button">
                            <Grid Background="Transparent">
                                <StackPanel Orientation="Horizontal">
                                    <ContentPresenter x:Name="ContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" OpticalMarginAlignment="TrimSideBearings" TextLineBounds="Tight" VerticalAlignment="Center"/>
                                </StackPanel>
                                <Rectangle x:Name="FocusVisualWhite" IsHitTestVisible="False" Margin="-5" Opacity="0" StrokeDashOffset="1.5" StrokeEndLineCap="Square" Stroke="{ThemeResource FocusVisualWhiteStrokeThemeBrush}" StrokeDashArray="1,1"/>
                                <Rectangle x:Name="FocusVisualBlack" IsHitTestVisible="False" Margin="-5" Opacity="0" StrokeDashOffset="0.5" StrokeEndLineCap="Square" Stroke="{ThemeResource FocusVisualBlackStrokeThemeBrush}" StrokeDashArray="1,1"/>
                            </Grid>
                        </ControlTemplate>
                    </Border.Resources>
                    <Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Rectangle x:Name="HubHeaderPlaceholder" Grid.Row="0"/>
                        <Button x:Name="HeaderButton" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" FontWeight="{ThemeResource HubSectionHeaderThemeFontWeight}" FontSize="{ThemeResource HubSectionHeaderThemeFontSize}" Margin="{ThemeResource HubSectionHeaderThemeMargin}" Grid.Row="1" Template="{StaticResource HeaderButtonTemplate}"/>
                        <ContentPresenter x:Name="ContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Grid.Row="2"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

参考

ストアアプリの単体テストプロジェクトで出力ディレクトリにコピーしたファイルを読み込む

サンプルデータとかを読み込みたいらしいです。

例えばテストプロジェクトに、TextFile1.txtという名前のファイルを作って中身を以下のようにします。

Hello world

コンテンツにして、出力フォルダにコピーするようにプロパティを構成します。

f:id:okazuki:20140803152934p:plain

このファイルを単体テスト内で読み込むには、Package,Current.InstallLocationのGetFileAsyncを使ってStorageFileを取得してごにょごにょします。とりあえずファイル内の文字列をアサートするならこんな感じ。

using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.ApplicationModel;

namespace UnitTestLibrary1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public async Task TestMethod1()
        {
            var file = await Package.Current.InstalledLocation.GetFileAsync("TextFile1.txt");
            Assert.IsNotNull(file);

            using (var r = await file.OpenReadAsync())
            using (var s = new StreamReader(r.AsStream()))
            {
                Assert.AreEqual("Hello world", (await s.ReadToEndAsync()).Trim());
            }
        }
    }
}

****Selector改良

改良といっても、本家のつもりんがちゃんとしたやつのコードを載せてくれた&そっちのほうが強力なので、そっちを使うといいとおもいます・・・!

私のやつは、XAMLの階層が深くなってださかったので、ResourceDictionaryを使うようにして普段DataTemplateを設定してるのと同じ要領で作れてx:Keyを型名にするようにしてみました。

[ContentProperty(Name = "Templates")]
public class DataTypeTemplateSelector : DataTemplateSelector
{
    private ResourceDictionary templates = new ResourceDictionary();

    public ResourceDictionary Templates
    {
        get { return templates; }
        set { templates = value; }
    }

    protected override DataTemplate SelectTemplateCore(object item)
    {
        if (item == null)
        {
            return null;
        }

        var dict = (IDictionary<object, object>)this.Templates;
        object result = null;
        bool ignore = dict.TryGetValue(item.GetType().Name, out result);
        return result as DataTemplate;
    }
}

使い方はこんな感じ。

<ListView ItemsSource="{Binding}">
    <ListView.ItemTemplateSelector>
        <local:DataTypeTemplateSelector>
            <DataTemplate x:Key="Person">
                <StackPanel>
                    <TextBlock Text="にんげんです" />
                    <TextBlock Text="{Binding Name}" />
                </StackPanel>                        
            </DataTemplate>
            <DataTemplate x:Key="Dog">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="いぬです" />
                    <TextBlock Text="{Binding Name}" />
                </StackPanel>
            </DataTemplate>
        </local:DataTypeTemplateSelector>
    </ListView.ItemTemplateSelector>
</ListView>

DataTemplate以外も置けてしまうのがメリットになるかデメリットになるか…。

Windows RuntimeのXAMLで、型に応じてDataTemplateを選択したい

id:tmytさんのアイデアです。

DataTemplateSelectorを実装して、状況に応じてDataTemplateを何個かの中から返すというのはよくやると思います。今回は、これの汎用実装的なかんじです。型名をキーにデータテンプレートを管理するという発想です。

/// <summary>
/// DataTemplate保持用クラス
/// </summary>
[ContentProperty(Name="DataTemplate")]
public class DataTemplateHolder
{
    public string TypeName { get; set; }
    public DataTemplate DataTemplate { get; set; }
}

/// <summary>
/// 型名をキーにDataTemplateを管理して返すDataTemplateSelector
/// </summary>
[ContentProperty(Name = "Templates")]
public class DataTypeDataTemplateSelector : DataTemplateSelector
{
    private List<DataTemplateHolder> templates = new List<DataTemplateHolder>();
    /// <summary>
    /// 型名とDataTemplateのリスト
    /// </summary>
    public List<DataTemplateHolder> Templates { get { return this.templates; } }

    protected override DataTemplate SelectTemplateCore(object item)
    {
        if (item == null)
        {
            return null;
        }
        // 登録してるDataTemplateに、対応する型名のものがあったらそれを返す
        var r = this.Templates.FirstOrDefault(i => i.TypeName == item.GetType().Name);
        return r != null ? r.DataTemplate : null;
    }
}

使い方

例えば、こんな2種類のクラスがあったとします。

public class Person
{
    public string Name { get; set; }
}

public class Dog
{
    public string Name { get; set; }
}

ListViewに先ほどのDataTypeDataTemplateSelectorを設定します。

<ListView ItemsSource="{Binding}">
    <ListView.ItemTemplateSelector>
        <local:DataTypeDataTemplateSelector>
            <local:DataTemplateHolder TypeName="Person">
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="Personだよ" />
                        <TextBlock Text="{Binding Name}" />
                    </StackPanel>
                </DataTemplate>
            </local:DataTemplateHolder>
            <local:DataTemplateHolder TypeName="Dog">
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="Dogだよ" />
                        <TextBlock Text="{Binding Name}" />
                    </StackPanel>
                </DataTemplate>
            </local:DataTemplateHolder>
        </local:DataTypeDataTemplateSelector>
    </ListView.ItemTemplateSelector>
</ListView>

ListViewのItemsSourceにデータを設定するため、ページのコンストラクタに以下のようなコードを書きます。

public MainPage()
{
    this.InitializeComponent();
    this.DataContext = Enumerable.Range(1, 100)
        .Select(i => i % 2 == 0 ?
            (object)new Person { Name = "にんげん" + i } :
            (object)new Dog { Name = "ぽち" + i });
}

実行すると、型に応じてテンプレートが切り替わってることが確認できます。

f:id:okazuki:20140706182946j:plain

こういうユーテリティ類は懐に用意しておくとよさげですね。