ViewMakerで作成するリアルアプリケーション フィードバックの反映

作成したアプリケーションをユーザに見せると、ユーザから要望やフィードバックを得られることが良くあります。簡単なレイアウト修正であればViewMakerを使うとその場で修正できるので相談しながら微調整を行います*1。その以外のよくあるフィードバックとして、操作感に関するものがあります。今回この操作感のフィードバックとして以下が指摘された仮定で対応していきたいと思います。

    1. 検索文字列でEnterを押下したら検索処理を実行してほしい
    2. 検索時に見つかったノードがスクロールして常に表示されるようにしてほしい
    3. 現在選択されているノードがツリービュー上で分かるようにしてほしい

Enter処理の追加

最初のEnter押下時に検索処理を実行するは、レイアウトエディターでFindStringアイテムのEnterKeyCommandをFindに設定するだけでできます。


TreeViewのスクロール処理

2番目は新しいビヘイビアを作成すると対応できそうです。以下ビヘイビアのコードです。新しいビヘイビアはapp.xamlで TreeViewに対して Style 指定で適用できます。

public static class TreeViewItemBringIntoViewBehavior
{
    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(TreeViewItemBringIntoViewBehavior), new PropertyMetadata(IsEnabledCallback));

    public static bool GetIsEnabled(TreeView obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(TreeView obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    private static void IsEnabledCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var ele = obj as TreeView;
        if (ele != null)
        {
            if ((bool)args.OldValue)
            {
                ele.RemoveHandler(TreeViewItem.SelectedEvent, new RoutedEventHandler(OnTreeViewItemSelected));
            }
            if ((bool)args.NewValue)
            {
                ele.AddHandler(TreeViewItem.SelectedEvent, new RoutedEventHandler(OnTreeViewItemSelected));
            }
        }
    }

    static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
    {
        TreeViewItem item = e.OriginalSource as TreeViewItem;
        if (item != null) item.BringIntoView();
    }

}
<Application x:Class="ViewMaker.Samples.ReflectionViewer.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:local="clr-namespace:ViewMaker.Samples.ReflectionViewer"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Startup="Application_Startup">
    <Application.Resources>
        <Style TargetType="{x:Type TreeView}">
            <Setter Property="local:TreeViewItemBringIntoViewBehavior.IsEnabled" Value="True" />
        </Style>
    </Application.Resources>
</Application>


ツリービューアイテムのスタイル指定

3番目はスタイルの適用は、生成されたTreeViewにすでにStyle適用されているのでapp.xamlのスタイルから適用することができません。このような場合、最後の手段で一旦XAMLを生成してUserControlに張り付けてXAMLを編集して対処します。*2

<UserControl x:Class="ViewMaker.Samples.ReflectionViewer.ReflectionViewerView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    
    <!--UserControlの直下をViewMakerで生成されたXAMLで置き換える -->
    <ContentControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib" IsTabStop="False">
        <ContentControl.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="/ViewMaker.Core;component/wpf/wpfgenericresource.xaml" />
                    <ResourceDictionary Source="/ViewMaker.Core;component/wpf/wpferrorstyleresource.xaml" />
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
        </ContentControl.Resources>
        <Grid Name="ReflectionViewerViewModel" DataContext="{Binding Mode=OneWay}">
            <Grid Name="ReflectionViewerViewModelDataPanel" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="0" Grid.Column="0">
                <StackPanel Orientation="Horizontal" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="0" Grid.Column="0">
                    <Grid HorizontalAlignment="Left">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="auto" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Label Width="100" HorizontalContentAlignment="Left" VerticalAlignment="Center">検索文字列</Label>
                        <TextBox Name="FindString" Text="{Binding Path=FindString, Mode=TwoWay, ValidatesOnExceptions=True, ValidatesOnDataErrors=True}" vg:EnterKeyBehavior.Command="{Binding Path=Find, Mode=OneWay}" Width="150" VerticalAlignment="Center" Grid.Column="1" xmlns:vg="clr-namespace:ViewMaker.Core.Wpf;assembly=ViewMaker.Core" />
                    </Grid>
                    <Button Name="Find" Command="{Binding Path=Find,Mode=OneWay}">検索</Button>
                </StackPanel>
                <TreeView Name="Children" ItemsSource="{Binding Path=Children,Mode=OneWay}" vg:TreeViewSelectedItemChangedBehavior.Command="{Binding Path=SelectedItemChangedCommand, Mode=OneWay}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="1" Grid.Column="0" xmlns:vg="clr-namespace:ViewMaker.Core.Wpf;assembly=ViewMaker.Core">
                    <TreeView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Path=Children,Mode=OneWay}">
                            <TextBlock Text="{Binding Path=Name,Mode=OneWay}" />
                        </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                    <TreeView.ItemContainerStyle>
                        <Style TargetType="{x:Type TreeViewItem}">
                            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
                            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
                            
                            <!--追加のスタイルの適用-->
                            <Setter Property="FontWeight" Value="Normal" />
                            <Style.Triggers>
                                <Trigger Property="IsSelected" Value="True">
                                    <Setter Property="FontWeight" Value="Bold" />
                                </Trigger>
                            </Style.Triggers>
                            
                        </Style>
                    </TreeView.ItemContainerStyle>
                </TreeView>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
            </Grid>
            <StackPanel Name="ContentPanel" Orientation="Vertical" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="0" Grid.Column="1">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <Label Width="100" HorizontalContentAlignment="Left" VerticalAlignment="Center" HorizontalAlignment="Left">詳細情報</Label>
                    <ContentControl Name="CurrentContent" Content="{Binding Path=CurrentContent, Mode=OneWay}" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" VerticalAlignment="Center" Grid.Row="1" />
                </Grid>
            </StackPanel>
            <StackPanel Name="ReflectionViewerViewModelButtonPanel" Orientation="Horizontal" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="1" Grid.Column="0">
                <Button Name="Load" Command="{Binding Path=Load,Mode=OneWay}" Margin="5">開く</Button>
                <Button Name="Claer" Command="{Binding Path=Claer,Mode=OneWay}" Margin="5">クリア</Button>
            </StackPanel>
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
        </Grid>
    </ContentControl>
</UserControl>


作成したUserControlの型をViewModelに関連づけるのはレイアウトエディターでも可能ですが、ViewModelの属性で管理できたほうがC#コードとして一元的に管理しやすいので以下のようにWindowの大きさと合わせて指定しておきます。

[View(typeof(ReflectionViewerView), Title = "リフレクションビューア")]
[ViewProperty(UserControlViewControl.Properties.WindowHeight, 500)]
[ViewProperty(UserControlViewControl.Properties.WindowWidth, 600)]
public class ReflectionViewerViewModel : ViewModel
{
    [Display(Name = "検索文字列")]
    [View(ViewControlType.TextBox)]
    public string FindString { get; set; }

あとは、レイアウト用のXMLファイルを削除しViewModelからViewを生成させることで、UserControlが利用されるようになります。

*1:場合によってはレイアウトの修正をユーザ自身でViewMakerを使って直接行ってもらえるかもしれません。もしこれができればコミュニケーションコストが下がり大きなメリットになります。個人的には実はこのシナリオができないかを狙っています

*2:この場合、2番目のビヘイビアの処理をUserControlのコードビハンドで直接書くことも可能で、実務的にはこちらの開発シナリオのほうを採用することになるでしょう