Prism.CommandsのDelegateCommand<T>で、VisualStudioのデザイナーが例外を吐く
また躓いたからメモ
現象
Windowクラス(を継承した独自のMyWindowクラス)内のButtonコントロールのCommandに、ViewModel側で用意したDelegateCommand
CommandParameterのバインドで、ElementName引数に渡したいWindowのElementNameを指定してバインドを試みるもVisualStudioのデザイナーがインスタンスを生成するときに例外を吐き、デザイナーが表示されない問題に遭遇した。
やりたかったこと
コマンドを実施するボタンが所属するウィンドウオブジェクト自体をCommandParameterとして渡したい。
背景
ボタンを処理内で、ボタンが所属するWindowクラス(を継承した独自のMyWindowクラス)をクローズしたかった。
例外がスローされました。
InvalidCastException: 型 'Microsoft.VisualStudio.DesignTools.WpfDesigner.InstanceBuilders.WindowInstance' のオブジェクトを型 'testNamespace.MyWindow にキャストできません。
StackTrace
場所 Prism.Commands.DelegateCommand`1.<>c__DisplayClass1_0.<.ctor>b__1(Object o)
場所 Prism.Commands.DelegateCommandBase.CanExecute(Object parameter)
場所 Prism.Commands.DelegateCommandBase.System.Windows.Input.ICommand.CanExecute(Object parameter)
場所 MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource(ICommandSource commandSource)
場所 System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute()
場所 System.Windows.Controls.Primitives.ButtonBase.HookCommand(ICommand command)
場所 System.Windows.Controls.Primitives.ButtonBase.OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
場所 System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
場所 System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
場所 System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
場所 System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
場所 System.Windows.DependencyObject.InvalidateProperty(DependencyProperty dp, Boolean preserveCurrentValue)
場所 System.Windows.Data.BindingExpressionBase.Invalidate(Boolean isASubPropertyChange)
場所 System.Windows.Data.BindingExpression.TransferValue(Object newValue, Boolean isASubPropertyChange)
場所 System.Windows.Data.BindingExpression.Activate(Object item)
場所 System.Windows.Data.BindingExpression.AttachToContext(AttachAttempt attempt)
場所 System.Windows.Data.BindingExpression.MS.Internal.Data.IDataBindEngineClient.AttachToContext(Boolean lastChance)
場所 MS.Internal.Data.DataBindEngine.Task.Run(Boolean lastChance)
場所 MS.Internal.Data.DataBindEngine.Run(Object arg)
場所 MS.Internal.Data.DataBindEngine.OnLayoutUpdated(Object sender, EventArgs e)
場所 System.Windows.ContextLayoutManager.fireLayoutUpdateEvent()
場所 System.Windows.ContextLayoutManager.UpdateLayout()
場所 System.Windows.UIElement.UpdateLayout()
<Window x:Class="testNamespace.MyWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:testNamespace" x:Name="myWindow" Height="350" Width="500"> <Grid> <Button x:Name="button_Output" Content="出力" Style="{StaticResource commandButtonStyle}" IsEnabled="{Binding CommandButtonEnabled}" Command="{Binding CommandButtonClickCommand}" CommandParameter="{Binding ElementName=myWindow}" Margin="0,0,4,4" HorizontalAlignment="Right" VerticalAlignment="Bottom" /> </Grid>
発見した解決方法
- キャストさせないように、object型で受け取るようにした
なんだかよくわからないけどキャストに失敗してるらしいから、
キャストさせないように、XAML側のCommandParameterのobject型に合わせ、
パラメータ受け取り側のパラメータの型をobject型にしてみたらうまくいった
DelegateCommand<MyWindow> → DelegateCommand<object>
- XAML側のバインド指定方法で、RelativeSourceを使った
RelativeSourceで同じオブジェクト内の別プロパティ(ここではWindow)を見つけさせたらよくわからないけどうまくいった
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
キャストに失敗 という判定が下る理由がまだよくわかってない。
その他試したけど無駄だったこと
- CanExecuteを用意していなかったため用意して、問答無用でtrueを返すようにした
https://msdn.microsoft.com/en-us/library/gg431410%28v=pandp.50%29.aspx
The constructor deliberately prevents the use of value types. Because ICommand takes an object, having a value type for T would cause unexpected behavior when CanExecute(null) is called during XAML initialization for command bindings.
Examples
public MyClass()
{
this.submitCommand = new DelegateCommand(this.Submit, this.CanSubmit);
}private bool CanSubmit(int? customerId)
{
return (customerId.HasValue && customers.Contains(customerId.Value));
}
英語力ないので誤訳してそうでアレですが、
値型は使うな。null許容型をparameterで渡すとよからぬことが起こりかねないから。
もし使いたいなら、CanExecute的なとこでチェックしろ と言っているのだと理解した。(すごく自信がない)
そして、例を見た感じ、CanSubmit(CanExecuteにあたるもの)でnullじゃなかったらtrueみたいに渡してるので
true判定を出せばとりあえずインスタンス化はできるのではないかと考えて実施
思うところ
- Object型がキャストできない型ってあるの?
- XAML側で型を指定することってできるの?
(調べた感じマークアップを使うのかな?でもDelegateCommandにはマークアップ拡張が用意されてなさそうな感じする)
- RelativeSource指定でうまくいくのは、AncestorType={x:Type Window}と指定していて、CommandParameterに入れた時点でWindow型になっている(???)からか
- 「InvalidCastException: 型 'Microsoft.VisualStudio.DesignTools.WpfDesigner.InstanceBuilders.WindowInstance' のオブジェクトを型 'testNamespace.MyWindow にキャストできません。」の型 'Microsoft.VisualStudio.DesignTools.WpfDesigner.InstanceBuilders.WindowInstance'ってデザイナー用の型っぽい。デザイナーはなんか違うオブジェクトを渡そうとしてる???
- 自分はやはりサーチ力英語力が低い。なかなかこれだという情報にたどり着けない。