none
メインウインドウにフレーム内のページから変更通知を行う。 RRS feed

  • 質問

  • VisualStudio2010とC#で開発しています。

    メインウインドウ内のフレームにページを配置し、ページ内のコマンドを実行した際、メインウインドウに配置したTextBlockのContentを変更したい場合、MVVMパターンではどのように変更通知を行えばよいのでしょうか?

    メインウインドウとページはそれぞれViewとViewModelに分かれています。初歩的な質問かもしれませんがよろしくお願いします。

    2015年11月27日 18:50

すべての返信

  • こんにちは。

    メインウインドウとフレームページのViewModel生成方法はどうなっているのでしょうか。
    Viewが親子関係になっているように、ViewModelも親子関係になっているのでしょうか、
    それともフレームのViewModelを管理するインスタンスが存在しますか。

    いずれにせよ、メインウインドウとページにバインドされているViewModelが存在するのであれば、
    ページのViewModelから、メインウインドウのViewModelへイベントなどを使って通知を行うだけだと思います。

    2015年11月28日 4:59
    モデレータ
  • 返信ありがとうございます。

    ViewModelの作成方法は以下の様にXAMLに記述してバインドしています。

    親子関係については現状、理解不足で特に意識していない状況です。

    メインウインドウ

    <Window.DataContext>
            <vm:MainWindowViewModel/>
     </Window.DataContext>

    フレームページ

     <Page.DataContext>
            <vm:Page1ViewModel/>
     </Page.DataContext>

    2015年11月28日 6:36
  • MainWindowへPage1をどのように配置していますか。

    単純にXAMLで静的に入れ子にしているかんじですか?

    2015年11月28日 8:19
    モデレータ
  • 説明不足で申し訳ありません。

    細かく書きますとFrame内で複数のPageを遷移させる動きになっています。

    メインウインドウの上半分にFrameを配置し、下半分にTextBlockやボタン等を配置しています。

    Pageの遷移方法は2パターンあります。

    1,メインウインドウ内のボタンを押してPageを切り替える。

    2,Page内のボタン押下やラベルのマウスクリックで他のPageに切り替える。

    初期ページはXAMLでFrameのSourceプロパティ設定しています。

    その後のPageの切替はViewにコードビハインドでFrameのNavigationServiceにてPageを切り替えています。

    (ここもコードビハインドで記述したくはないのですが、方法が分かりません。)

    2015年11月28日 9:10
  • コードビハインドをやめて、Messengerの仕組みなどで、MainWindowViewModelにて遷移先のViewModelを管理できるようにしたほうが良いのではないでしょうか。

    MainWindowの中にViewが入れ子になっているのであれば、MainWindowViewModelの中にPage1ViewModelが入れ子になっていても良い気がします。

    2015年11月28日 13:49
    モデレータ
  • 作成されているアプリが、比較的コンパクトなものであれば、ですが。

    私だったらページ毎のViewModelを定義するのをやめて、アプリ内に唯一存在するMainViewModelをすべてのViewがContentsに設定するような設計にします。

    Pageというより、MainViewの一部として扱うようなイメージです。実際のコードは下記のような感じです。

    1. MainViewModelには、ページ内のコマンドと、メインウィンドウのTextBlockに表示させたいStringの通知プロパティを定義
    2. メインウィンドウ内のTextBlockと、MainViewModelのStoringプロパティをBinding
    3. コマンドが実行されたら、Stringプロパティを更新

    もちろん、この方法はViewModelが巨大化し、複数人で各ページを実装するような作業分担がしにくくなるなど、弊害もあります。
    ただ、MVVMの勉強しながら、コンパクトなアプリを作るのであれば、この方がいろいろ悩まずに簡単に実現できます。

    まずはきちんとViewとViewModelを分離して、すべてBindingでコントロールできるクセを付けるのが先かなと思います。

    その上で、「もっとViewModelは開発体制やアプリ構造にあわせて分離しなきゃ」と自覚したときに、改めて見直してもいいんじゃないでしょうか?

    ViewとViewModelがきちんと分かれていれば、後からViewModelを階層化・並列化させることは、さほど難しくないと思います。もちろん、そのタイミングで子VMから親VMへ情報を伝搬させる方法は考えないといけませんが。





    • 編集済み NIM5 2015年11月28日 16:52
    2015年11月28日 16:45
  • コマンド実行すると何かプロパティが変わったりするので、それをメイン側で表示するだけのような。
    "コマンドの実行"自体を通知するなら、実行したというプロパティで通知するなど。

    FrameのContentプロパティからPageが取れるので、そのDataContextをメイン側ViewにあるContentPresenterかContentControlのContentにバインドして、あとはDataTemplateでViewModelの型ごとに表示を切り替えてやるとか。
    これならページ側はメイン側を全く知らなくてもいい。

    あるいは面倒だけどFrameのContentをViewModel側にバインドで通知して、MainのViewModelではそのDataContextでコマンド実行されたことを検出する処理を入れてやるとか。
    こっちはページ外部に伝える必要があることをページが知っていないといけない。

    <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="500" Width="305">
        <Window.DataContext>
            <app:MainViewVM />
        </Window.DataContext>
        <UniformGrid Rows="2">
            <Frame x:Name="frame1" NavigationUIVisibility="Visible" Source="{Binding Path=Source,Mode=TwoWay}">
                <Frame.CommandBindings>
                    <CommandBinding Command="{x:Static NavigationCommands.GoToPage}"
                                    Executed="CommandBinding_Executed"/>
                </Frame.CommandBindings>
                <Frame.Content>
                    <Binding Path="DataContext" Mode="OneWayToSource">
                        <Binding.Converter>
                            <app:DataContextConverter />
                        </Binding.Converter>
                    </Binding>
                </Frame.Content>
            </Frame>
    
            <StackPanel Background="Black" TextElement.Foreground="White">
                <GroupBox Header="ViewModel経由でごちゃごちゃと" Margin="10">
                    <StackPanel>
                        <TextBlock Text="{Binding Path=Source}" Margin="5"/>
                        <TextBlock Text="{Binding Path=DataContext}" Margin="5"/>
                        <TextBlock Text="{Binding Path=Message}" Margin="5"/>
                    </StackPanel>
                </GroupBox>
    
                <GroupBox Header="View層で切り替え" BorderBrush="LightGray" Margin="10"
                          DataContext="{Binding Path=Content.DataContext,ElementName=frame1}">
                    <StackPanel >
                        <ItemsControl ItemsSource="{Binding Path=Commands}">
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <WrapPanel />
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <Button Command="{Binding Value}" Content="{Binding Path=Key}" Margin="2"/>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
    
                        <ContentPresenter Content="{Binding Path=.}">
    
                            <ContentPresenter.Resources>
                                <DataTemplate DataType="{x:Type app:Page1VM}" >
                                    <StackPanel>
                                        <Slider Minimum="0" Maximum="10000" TickFrequency="1" Value="{Binding Path=Value}"/>
                                        <TextBlock Text="{Binding Path=Message}"  />
                                    </StackPanel>
                                </DataTemplate>
                                <DataTemplate DataType="{x:Type app:Page2VM}" >
                                    <StackPanel>
                                        <TextBlock Text="{Binding Path=Now,StringFormat=HH:mm:ss}" />
                                    </StackPanel>
                                </DataTemplate>
                            </ContentPresenter.Resources>
                        </ContentPresenter>
                    </StackPanel>
                </GroupBox>
            </StackPanel>
        </UniformGrid>
    </Window>
    <Page x:Class="WpfApplication1.Page1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:app="clr-namespace:WpfApplication1"
    	Title="Page1">
        <Page.DataContext>
            <app:Page1VM />
        </Page.DataContext>
        <StackPanel>
            <Button Command="{x:Static NavigationCommands.BrowseBack}" Content="<"/>
            <TextBlock><Hyperlink NavigateUri="Page2.xaml">Page2</Hyperlink></TextBlock>
    
            <TextBlock Text="{Binding Path=Value}" Margin="20"/>
            <Button Command="{Binding Path=TestCommand}" Content="x10"/>
        </StackPanel>
    </Page>
    
    <Page x:Class="WpfApplication1.Page2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:WpfApplication1"
    	Title="Page2">
        <Page.DataContext>
            <app:Page2VM />
        </Page.DataContext>
        <StackPanel>
            <Button Command="{x:Static NavigationCommands.BrowseBack}" Content="<"/>
            <Button Command="{x:Static NavigationCommands.GoToPage}" CommandParameter="Page3.xaml"  Content="Page3"/>        
            <TextBlock Text="{Binding Path=Now}" Margin="50" Language="ja-jp"/>
        </StackPanel>
    </Page>
    
    <Page x:Class="WpfApplication1.Page3" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:WpfApplication1"
    	Title="Page3">
        <Page.DataContext>
            <app:Page3VM />
        </Page.DataContext>
        <StackPanel>
            <Button Command="{x:Static NavigationCommands.BrowseBack}" Content="<"/>
            
            <Button Command="{Binding Path=TestCommand}" Content="Test" />
        </StackPanel>
    </Page>
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Input;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow() { InitializeComponent(); }
    
            private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
            {
                ((Frame)sender).Navigate(e.Parameter as Uri ?? new Uri(e.Parameter.ToString(), UriKind.Relative));
            }
        }
    
        class ModelBase : INotifyPropertyChanged, ICommandExecuted
        {
            public ModelBase ()
    	{
            Commands = new Dictionary<string, ICommand>();
    	}
    
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
            protected virtual void OnPropertyChanged(string name)
            {
                var pc = PropertyChanged;
                if (pc != null) { pc(this, new System.ComponentModel.PropertyChangedEventArgs(name)); }
            }
    
            public event EventHandler<CommandExecutedEventArgs> CommandExecuted;
            protected void OnCommandExecuted(string commandName)
            {
                var ce = CommandExecuted;
                if (ce != null) { ce(this, new CommandExecutedEventArgs(commandName)); }
            }
    
            public Dictionary<string,ICommand> Commands{get;private set;}
            
        }
    
        class MainViewVM : ModelBase
        {
            /// <summary>Frameに表示されている(させたい)ページのUri</summary>
            public Uri Source
            {
                get { return _Source; }
                set { if (_Source != value) { _Source = value; OnPropertyChanged("Source"); } }
            }
            private Uri _Source = new Uri("Page1.xaml", UriKind.Relative);
    
            /// <summary>Converter経由でFrameに表示されているページのDataContextを受け取る</summary>
            public object DataContext
            {
                get { return _DataContext; }
                set
                {
                    if (_DataContext != value)
                    {
                        var npc = _DataContext as INotifyPropertyChanged;
                        if (npc != null) { npc.PropertyChanged -= OnFrameDataContextChanged; }
                        var ce = _DataContext as ICommandExecuted;
                        if (ce != null) { ce.CommandExecuted -= OnFrameCommandExecuted; }
                        _DataContext = value;
    
                        npc = _DataContext as INotifyPropertyChanged;
                        if (npc != null) { npc.PropertyChanged += OnFrameDataContextChanged; }
                        ce = _DataContext as ICommandExecuted;
                        if (ce != null) { ce.CommandExecuted += OnFrameCommandExecuted; }
                        Message = "";
                        OnPropertyChanged("DataContext");
                    }
                }
            }
    
    
            private object _DataContext;
    
            /// <summary>
            /// FrameのDataContextが何か変化したら呼ばれる
            /// コマンドを実行すれば何かプロパティが変化するならこっちだけで済む
            /// </summary>
            private void OnFrameDataContextChanged(object sender, PropertyChangedEventArgs e)
            {
                var value = sender.GetType().GetProperty(e.PropertyName).GetValue(sender, null);
                Message = string.Format("{0}:{1}={2}", sender, e.PropertyName, value);
            }
    
            /// <summary>コマンドを実行したけど何もプロパティが変化しない場合でも"実行した"ことを知りたい場合</summary>
            void OnFrameCommandExecuted(object sender, CommandExecutedEventArgs e)
            {
                Message = string.Format("{0}:{1} Executed!", sender, e.CommandName);
            }
    
            public string Message
            {
                get { return _Message; }
                set { if (_Message != value) { _Message = value; OnPropertyChanged("Message"); } }
            }
            private string _Message;
        }
    
        class Page1VM : ModelBase
        {
            public Page1VM()
            {
                TestCommand = new DelegateCommand((o) => { Value *= 10; Message = "Valueを10倍にしました"; });
                base.Commands.Add("x10", TestCommand);
    
                base.Commands.Add("/10",new DelegateCommand((o) => { Value /= 10; Message = "Valueを1/10倍にしました"; }));
            }
            public int Value
            {
                get { return _Value; }
                set { if (_Value != value) { _Value = value; OnPropertyChanged("Value"); } }
            }
            private int _Value=10;
    
            public ICommand TestCommand { get; private set; }
    
            public string Message
            {
                get { return _Message; }
                set { if (_Message != value) { _Message = value; OnPropertyChanged("Message"); } }
            }
            private string _Message;
        }
    
        class Page2VM : ModelBase
        {
            private System.Windows.Threading.DispatcherTimer timer;
    
            public Page2VM()
            {
                timer = new System.Windows.Threading.DispatcherTimer();
                timer.Interval = TimeSpan.FromSeconds(1);
                timer.Tick += (s, e) => { OnPropertyChanged("Now"); };
                timer.Start();
            }
    
            public DateTime Now { get { return DateTime.Now; } }
        }
    
        class Page3VM : ModelBase
        {
            public Page3VM()
            {
                TestCommand = new DelegateCommand((o) =>{ OnCommandExecuted("TestCommand"); });
                base.Commands.Add("Test",TestCommand);
            }
            public ICommand TestCommand { get; private set; }
        }
    
        /// <summary>FrameworkElementに設定されているDataContextをソース側に伝えるコンバーター</summary>
        /// <author></author>
        /// <remarks></remarks>
        /// <date>2015/11/29</date>
        [ValueConversion(typeof(FrameworkElement), typeof(object))]
        class DataContextConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                return value;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (value is FrameworkElement)
                {
                    return ((FrameworkElement)value).DataContext;
                }
                return null;
            }
        }
    
        #region コマンドを"実行した"ことをViewModel層で伝えるためのインターフェース</summary>
        public interface ICommandExecuted
        {
            event EventHandler<CommandExecutedEventArgs> CommandExecuted;
        }
        public class CommandExecutedEventArgs : EventArgs
        {
            public CommandExecutedEventArgs(string commandName) { this.CommandName = commandName; }
            public string CommandName { get; private set; }
        }
        public class DelegateCommand : ICommand
        {
            public DelegateCommand(Action<object> a)
            {
                this._Action = a; 
            }
            private Action<object> _Action;
            public event EventHandler CanExecuteChanged;
            public bool CanExecute(object parameter) 
            {
                return true; 
            }
            public void Execute(object parameter) { _Action(parameter); }
        }
        #endregion
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2015年11月29日 4:17
  • 簡単にやるなら、ページのViewModelにメインウインドウのViewModelを渡せばよいように思いますね。強く型に依存するのが嫌な場合は、インターフェース経由にしても良いでしょう。

    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2015年11月30日 1:31
    モデレータ