RunningCSharp

MS系開発者による、雑多な記事。記事は所属企業とは関係のない、個人の見解です。

C#で書いたカスタム属性をVB(.net)でも書いてみる

VBを使い慣れない私が、カスタム属性クラスを適用するテストコードをC#で書いた後、そのコードVBに書き直してみただけの記事です。

C#

    //文字列を保持するだけの属性
    public class TestAttribute : Attribute
    {
        private string val;
        //属性のコンストラクタ
        public TestAttribute(string testValue)
        {
            val = testValue;
        }
        //コンストラクタで入れた文字列を返すプロパティ
        public string Value
        {
            get { return val; }
        }
    }
    //Test属性を適用したクラス
    [Test("TestValue")]
    public class TestClass
    {
    }

    class Program
    {
        static void Main(string[] args)
        {
            //TestClassに指定されたTestAttributeを取得
            var att = (TestAttribute)Attribute.GetCustomAttribute(typeof(TestClass), typeof(TestAttribute));
            //TestAttributeのValueを取得
            Console.WriteLine(att.Value);
        }
    }

上記処理をVB(Visual Studio 2015)で書き直してみます。

'文字列を保持するだけの属性
Public Class TestAttribute
    Inherits System.Attribute
    Private val As String
    '属性のコンストラクタ
    Public Sub New(ByVal testValue As String)
        val = testValue
    End Sub
    'コンストラクタで入れた文字列を返すプロパティ
    Public ReadOnly Property Value() As String
        Get
            Return val
        End Get
    End Property
End Class

'Test属性を適用したクラス
<Test("TestValue")>
Public Class TestClass
End Class

Module Module1

    Sub Main()
        'TestClassに指定されたTestAttributeを取得
        Dim att As TestAttribute = Attribute.GetCustomAttribute(GetType(TestClass), GetType(TestAttribute))
        'TestAttributeのValueを取得
        Console.WriteLine(att.Value)
    End Sub

End Module

書いてみた結果、継承、コンストラクタ、プロパティ定義やら、C#VBでの書き方の違いを見比べられて少し良いなと思いました。

C#ユーザーがVBの遅延バインディングに驚いた話など

色々あって、Visual Studio 2008でVBをやっています。

触ってみた結果、多数のスタティックメソッドや「改行にアンダーバー必須」といった文法関連など色々なC#との使い勝手の違いを感じましたが、一番違いに驚いたのは下記コードの動作です。

    Dim testobj As Object
    'オブジェクト型の変数に日付型を代入
    testobj = DateTime.Now
    'Objectクラスが持っていない「Month」プロパティを、遅延バインディングで使える
    Console.WriteLine(testobj.Month & "月")

思い起こせば、VB6の頃は当たり前のように遅延バインディングを使ったコードを書いていましたが、今見るとtestobjにMonthプロパティがないクラスのインスタンスを入れると実行時エラーが出せてしまう事から、少々おっかない印象を受けてしまいます。

なお上記のコードをC#で動くように書くと、下記のようになります。

    object testobj;
    testobj = DateTime.Now;
    //TypeクラスのGetPropertyメソッドでプロパティ情報を取得し、オブジェクトのプロパティ値取得を行う
    Console.WriteLine(testobj.GetType().GetProperty("Month").GetValue(testobj).ToString() + "月");

リフレクションを用いてプロパティ情報を取得した後、プロパティの実際の値を取得する流れになります。

少々冗長な気もしますが、リフレクションを使うと下記のように、変数testobjのプロパティの有無をチェックする事が容易です。

    object testobj;
    testobj = DateTime.Now;
    //Monthプロパティを取得
    System.Reflection.PropertyInfo prop = testobj.GetType().GetProperty("Month");
    if (prop != null)
    {//testobjに入ったオブジェクトに、Monthプロパティが存在する場合にのみ処理が実行される
        Console.WriteLine(prop.GetValue(testobj).ToString() + "月");
    }

プロパティの有無をチェックすることで、実行時エラーを防げます。

もちろんVBでも、リフレクションを用いてプロパティの有無をチェックできます。

    Dim testobj As Object
    testobj = DateTime.Now
    'リフレクションでMonthプロパティの情報を取得
    Dim prop As System.Reflection.PropertyInfo = testobj.GetType().GetProperty("Month")
    If (prop <> Nothing) Then
        'testobjにMonthプロパティが存在する場合のみ、処理が実行される
        Console.WriteLine(testobj.Month & "月")
    End If

コードは長くなりますが、遅延バインディングの対象となるオブジェクト型の変数に何が入ってくるかわからない時にはぜひこのチェックは行ったほうが良さそうです。

遅延バインディングを止めて、きちんと型チェックをしてから実行したい場合は下記のような感じで。

Option Strict OnDim testobj As Object
    testobj = DateTime.Now
    'リフレクションでMonthプロパティの情報を取得
    Dim prop As System.Reflection.PropertyInfo = testobj.GetType().GetProperty("Month")
    If (prop <> Nothing) Then
        'testobjにMonthプロパティが存在する場合のみ、処理が実行される
        Console.WriteLine(prop.GetValue(testobj).ToString() & "月")
    End If

なお、C#4.0からはdynamic型が使えるため、C#でも遅延バインディング的な書き方が可能です。

    dynamic testobj;
    testobj = DateTime.Now;
    Console.WriteLine(testobj.Month + "月");

VBの遅延バインディングと同じ理由で、testobjの内容に変更が多い場合はリフレクションのほうがお勧めです。

WPF:プロパティ値の継承(包含継承)が可能な添付プロパティ

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <GroupBox Header="test" Foreground="Red">
        <StackPanel Orientation="Vertical">
            <TextBlock Text="test" />
            <TextBox Text="test"/>
            <Button Content="test" />
        </StackPanel>
    </GroupBox>
</Window>

上記のようなxamlを実行した場合、GroupBoxのForegroundプロパティの値を子のアイテムが引き継ぐことがあります。 この動きを、プロパティの値の継承と呼びます。(クラスの派生による継承とは異なることから、「包含継承」と呼ぶことがあるようです。)

上記の例では、TextBlockはGroupBoxのForegroundを引き継ぎますが、TextBoxやButtonのForegroundは引き継がれないようです。

f:id:ys-soniclab:20160910215003p:plain

そこで、プロパティ値の継承が可能な添付プロパティを作成し、そのプロパティが設定されたコントロールのForegruondを強制的に書き換えるようにしてみます。

public class Att
{
    public static Brush GetForeground(DependencyObject obj)
    {
        return (Brush)obj.GetValue(ForegroundProperty);
    }

    public static void SetForeground(DependencyObject obj, Brush value)
    {
        obj.SetValue(ForegroundProperty, value);
    }

    public static readonly DependencyProperty ForegroundProperty =
        DependencyProperty.RegisterAttached("Foreground", typeof(Brush), typeof(Att), new FrameworkPropertyMetadata(null, OnForegroundChanged) { Inherits = true });

    private static void OnForegroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = d as Control;
        if (ctl == null) return;
        ctl.Foreground = e.NewValue as Brush;
    }
}
<Window …>
    <GroupBox Header="test" local:Att.Foreground="Red">
        <StackPanel Orientation="Vertical">
            <TextBlock Text="test" />
            <TextBox Text="test"/>
            <Button Content="test" />
        </StackPanel>
    </GroupBox>
</Window>

上記の添付プロパティを付けると、Foregroundが強制的に変更されます。

f:id:ys-soniclab:20160910215020p:plain

Xamarin:Key指定なしのStyle(デフォルトのStyle)を継承する方法

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="App.MainPage"
             xmlns:local="clr-namespace:App">
  <ContentPage.Resources>
    <ResourceDictionary>
      <Style TargetType="Button">
        <Setter Property="BackgroundColor" Value="Red"/>
      </Style>
    </ResourceDictionary>
  </ContentPage.Resources>
  
  <StackLayout Orientation="Vertical">
    <Button Text="Test1"/>
    <Button>
      <Button.Style>
        <Style TargetType="Button">
          <Setter Property="Text" Value="Test2"/>
        </Style>
      </Button.Style>
    </Button>
  </StackLayout>
</ContentPage>

上記のようなxamlを記述した場合、ContentPageの子要素でButtonを定義した場合、基本的には自動的にContent.Resourcesで定義したKeyを指定していないStyleが適用されます。そのため、上のボタンは何も指定していなくともBackgroundが赤くなります。

しかし、下のボタンはButtonのStyleプロパティに新規でStyleを指定しているため、Content.Resourcesで定義したStyleが適用されません。

f:id:ys-soniclab:20160906222106p:plain

下のボタンにもContent.ResourcesのStyle(つまり、その場所での暗黙的なStyle)を継承させるには、

WPFではBasedOn="{StaticResource {x:Type Button}}"のような書き方をしましたが、Xamarinでは下記のようにBasedOnを記述します。

    <Button>
      <Button.Style>
        <Style TargetType="Button" BasedOn="{StaticResource Xamarin.Forms.Button}">
          <Setter Property="Text" Value="Test2"/>
        </Style>
      </Button.Style>
    </Button>

Buttonを上記のように書き換えると、標準のStyleを継承することが出来ます。

f:id:ys-soniclab:20160906222120p:plain

Xamarin:PCLに埋め込みリソースとしてテキストなどのファイルを配置し、そのファイルを読み込む

PCLプロジェクトに「埋め込みリソース」としてファイルを配置し、そのファイルを読み込む例を記載します。 今回の例はxmlファイルですが、テキストでも同様の方法で読み込めます。

下記の配置のファイルを読み込む例です。

f:id:ys-soniclab:20160905213320p:plain

//typeofの引数には、このメソッドが実装されているクラスを指定してください
var assembly = typeof(MainPageViewModel).GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream("FlashCard.Data.initialData.xml"))
using (StreamReader reader = new StreamReader(stream))
{
    //下記例は両方実行しようとするとエラーになるため、文字列かxmlどちらかのみ実行してください。

    //文字列として読み込む例
    string str = await reader.ReadToEndAsync();
    //xmlをデシリアライズする例
    System.Xml.Serialization.XmlSerializer serializer =
        new System.Xml.Serialization.XmlSerializer(typeof(FlashCardList));
    var list = serializer.Deserialize(reader) as FlashCardList;
}

MVVM:とにかく適当なICommandを実装したい時のサンプル

サンプルプログラム作成時、INotifyPropertyChangedを手書きで実装したViewModelは作ったけれど、

「ICommandも実装しなきゃいけないの忘れてた!今からnugetでMVVM用ライブラリ落としてくるのも面倒くさいし…」

みたいな時の為の、しょうもないサンプルです。

public class RelayCommand : ICommand
{
    //Command実行時に実行するアクション、引数を受け取りたい場合はこのActionをAction<object>などにする
    private Action _action;

    public RelayCommand(Action action)
    {//コンストラクタでActionを登録
        _action = action;
    }

    #region ICommandインターフェースの必須実装

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {//とりあえずActionがあれば実行可能
        return _action != null;
    }

    public void Execute(object parameter)
    {//今回は引数を使わずActionを実行
        _action?.Invoke();
    }

    #endregion
}

私個人は上記実装を非常によく使います。

こんな感じで利用してください。

View

<Window x:Class="WpfApplication.MainWindow" … >
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Grid>
        <Button Content="Test" Command="{Binding MyCommand}"
                HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Grid>
</Window>

ViewModel

public class ViewModel : INotifyPropertyChanged
{
…
    private RelayCommand _myCommand;

    /// <summary>
    /// Gets the MyCommand.
    /// </summary>
    public RelayCommand MyCommand
    {
        get
        {
            return _myCommand
                ?? (_myCommand = new RelayCommand(
                () =>
                {
                    MessageBox.Show("clicked!");
                }));
        }
    }
}

実行するとこんな感じです。

f:id:ys-soniclab:20160903213348p:plain

WPF:DataGridヘッダーのクリック時にViewModel側のICommandを実行するための添付プロパティ

stackoverflow.com

上記リンクでBlendのオブジェクトの拡張を作成してTriggerをStyle内に記述する方法での解決があります。 汎用性があるものの、多くのxamlを書く必要があるため、添付プロパティでの解決方法も一応記載してみようと思いました。

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <DataGrid ItemsSource="{Binding Items}" />
    </Grid>
</Window>

Windowはこんな感じで、

namespace WpfApplication
{
    public class MainViewModel
    {
        public List<object> Items { get; } = new List<object>() { new { Item1 = 1, Item2 = "山田" }, new { Item1 = 2, Item2 = "田中" } };
    }
}

DataContextに入れるVMはこんな感じで。

f:id:ys-soniclab:20160902182112p:plain

この画面に、ヘッダークリックイベントを付けていきます。

namespace WpfApplication
{
    public class Attached
    {
        public static ICommand GetColumnHeaderClickCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(ColumnHeaderClickCommandProperty);
        }

        public static void SetColumnHeaderClickCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(ColumnHeaderClickCommandProperty, value);
        }

        // Using a DependencyProperty as the backing store for ColumnHeaderClickCommand.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColumnHeaderClickCommandProperty =
            DependencyProperty.RegisterAttached("ColumnHeaderClickCommand", typeof(ICommand), typeof(Attached), new PropertyMetadata(null, PropertyChanged));

        private static void PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var grid = d as DataGrid;
            grid.Loaded -= Grid_Loaded;
            if (e.NewValue != null)
                grid.Loaded += Grid_Loaded;
        }

        private static void Grid_Loaded(object sender, RoutedEventArgs e)
        {
            var grid = sender as DataGrid;
            grid.FindChildren<DataGridColumnHeader>().ToList().ForEach(col => col.Click += Col_Click);
        }

        private static void Col_Click(object sender, RoutedEventArgs e)
        {
            var header = sender as DataGridColumnHeader;
            var grid = header.FindParent<DataGrid>();
            GetColumnHeaderClickCommand(grid)?.Execute(null);
        }

    }

    public static class Extensions
    {
        public static T FindParent<T>(this DependencyObject child) where T : DependencyObject
        {
            DependencyObject parentObject = VisualTreeHelper.GetParent(child);
            if (parentObject == null) return null;
            T parent = parentObject as T;
            return parent != null ? parent : FindParent<T>(parentObject);
        }

        public static List<T> FindChildren<T>(this DependencyObject parent, List<T> children = null)
            where T : DependencyObject
        {
            if (children == null)
                children = new List<T>();

            if (parent != null)
            {
                int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
                for (int i = 0; i < childrenCount; i++)
                {
                    var child = VisualTreeHelper.GetChild(parent, i);
                    var typeChild = child as T;
                    if (typeChild != null)
                    {
                        children.Add(typeChild);
                    }
                    children = FindChildren<T>(child, children);
                }
            }
            return children;
        }

    }

}

上記のクラスを作成し、

<Window …><Grid>
        <DataGrid ItemsSource="{Binding Items}" local:Attached.ColumnHeaderClickCommand="{Binding HeaderClickCommand}"/>
    </Grid>
</Window>

上記のように添付プロパティをDataGridに追加し、

namespace WpfApplication
{
    public class MainViewModel
    {
…
        private RelayCommand _headerClickCommand;

        /// <summary>
        /// Gets the HeaderClickCommand.
        /// </summary>
        public RelayCommand HeaderClickCommand
        {
            get
            {
                return _headerClickCommand
                    ?? (_headerClickCommand = new RelayCommand(
                                          () =>
                                          {
                                              MessageBox.Show("header clicked");
                                          }));
            }
        }

    }
}

ViewModelに上記実装を加えると、クリック時にメッセージボックスが出るようになります。

f:id:ys-soniclab:20160902182131p:plain

xamlの変更は少ないのですが、添付プロパティがそこそこの大きさになってしまいました…