30
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WPFで非同期アプリ間通信アプリを作りました

Posted at

以下の内容は間違いや曲解が多く見受けられると思います。ご指摘コメント、プルリクエスト大歓迎です!

WPFって?

Windows Form に代わる GUI フレームワークという認識です。XMLを拡張したXAMLと呼ばれる言語でUIを記述します。ロジックとUIを分離することが可能なので、デザイナーとプログラマーの分業が可能だとかなんとか。

なんしか「イマドキのUIデザインが作成できるフレームワーク」なんだと思います。

非同期 ( async / await )

.Net Framework 4.5 から新たに加わったキーワードで、非同期処理を容易に書くことが可能となりました。

今までは、TCPコネクションの確率をするのに、相手側の応答を待つ必要があったため、UI部分の処理が停止してしまう問題がありました。それを回避するのに、UIと通信部分のスレッドを分け、通信スレッドが応答を待っている間もUI部分の処理をできるようにする処理をユーザー各自が記述する必要がありました。

今回の async / await キーワードを使用すると、そのあたりの記述が楽になります。ざっくり説明すると、非同期で動作させたいメソッドの定義に async をつけ、非同期で待ちたい場所に await をつけることでスレッドを意識して作らずとも非同期で通信させることができます。

async / await については以下のURLがわかりやすいです。
http://msdn.microsoft.com/ja-jp/library/hh191443.aspx

アプリ間通信

アプリ間のデータのやり取りにはプロセス間通信というものがあり、.Net には標準でその機能が備わっています。簡単に済ませたい方はそちらを参照していただいたほうがよいと思います。私は通信の勉強も兼ねているので、かなりイレギュラーな方法でアプリ間通信を実現していると思います。

プロセス間通信 : http://programmers.high-way.info/cs/ipc.html

概要

作成したアプリの概要を説明します。アプリはServerとClientに分かれており、Server側の値を変えると、Client側の値がリアルタイムで同期します。逆はまだできてません(^^;)
エラー処理もしてませんし、未完成品ですm(_ _)m

app_comm.png

Serverサイド

Serverサイドでは、Clientから要求があった際に自身の現在値を返すだけのものです。以下にコードを掲載します。説明はコメントとして記述しましたので、ご参照ください。

MainWindow.xaml
<Window x:Class="AppsCommunicationServer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Server" Height="148" Width="253">
    <Grid>
        <TextBox VerticalContentAlignment="Center"
                 HorizontalContentAlignment="Center"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Top"
                 Height="115"
                 Width="243"
                 FontSize="48"
                 Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"/>
        <!-- TextをValueプロパティにBindingしているので、テキストボックスで値を変更すると
             対応するValueプロパティが変更される。
             UpdateSourceTrigger は PropertyChanged にしている。
             こうすることで値を入力したタイミングで更新され、リアルタイムでClient側に値を送信できる -->
    </Grid>
</Window>
MainWindow.cs
using System.Windows;
using System.ComponentModel;

namespace AppsCommunicationServer
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            /// データコンテキストにServerValueを設定すると
            /// XAML側でServerValueのプロパティをバインディング
            /// できるようになる。バインディングは簡単に言うと
            /// UIとオブジェクト間で値をリンクすることができる
            /// 今回はValueプロパティをリンクしてテキストボックスと
            /// あたりをリンクしているので、テキストボックスで
            /// 値を変更すると、Valueプロパティも変化する。
            this.DataContext = new ServerValue();
        }
    }

    public class ServerValue : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        #region プロパティ
        /// <summary>
        /// 今回通信でやり取りするデータ部分
        /// Getter / Setter を持つ
        /// Setter で OnPropertyChangedを使うことで
        /// UIに変化通知が送信され、UIの値が更新される
        /// 今回の例ではServer再度はあまり意味がない
        /// </summary>
        private int _value;
        public int Value
        {
            get
            {
                return _value;
            }

            set
            {
                _value = value;
                OnPropertyChanged("Value");
            }
        }
        #endregion

        #region コンストラクタ
        public ServerValue()
        {
            Value = 0;

            var ipAdd = System.Net.Dns.GetHostEntry("localhost").AddressList[0];

            listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 12301);

            listener.Start();

            accept();
        }
        #endregion

        /// <summary>
        /// TCPClientのコネクション要求を受け入れる
        /// 受け入れるまで非同期で待つ
        /// 受け入れると acceptClient に処理を渡す
        /// </summary>
        async void accept()
        {
            while (true)
            {
                var client = await listener.AcceptTcpClientAsync();

                acceptClient(client);
            }
        }

        /// <summary>
        /// コネクションが確率されたクライアントに対して
        /// 要求が来るまで非同期で待つ。要求が来た場合、
        /// 要求に応じた応答をする。
        /// 応答をした後、再度要求が来るまで待つ。
        /// </summary>
        /// <param name="client"></param>
        async void acceptClient(System.Net.Sockets.TcpClient client)
        {
            var ns = client.GetStream();

            // 受信データ受けるようの箱を用意
            byte[] result_byte = new byte[256];

            do
            {
                // ネットワークストリームからデータが取れるまで待つ
                int result_size = await ns.ReadAsync(result_byte, 0, result_byte.Length);

                // サイズが0ならコネクションが切られたと判断し、クライアントをクローズ
                // 上位レイアで再度コネクションが来るまで待つ
                if (result_size == 0)
                {
                    client.Close();
                    return;
                }

                // なにがしかを受信したと判断しValueプロパティの値を文字列にして返す
                var enc = System.Text.Encoding.UTF8;
                var send_bytes = enc.GetBytes(Value.ToString()); // 送信する用の箱にデータを詰める
                ns.Write(send_bytes, 0, send_bytes.Length);

            } while (ns.DataAvailable);

            // 再度受信待ち状態にするために再帰呼出し
            acceptClient(client);
        }

        private System.Net.Sockets.TcpListener listener;

        /// <summary>
        /// 値が更新されたことをUIに伝えるためのメソッド
        /// name で指定されたプロパティが更新されたことを伝える。
        /// </summary>
        /// <param name="name"></param>
        private void OnPropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

Clientサイド

ClientサイドはServerに対して要求を送り、応答データを値データとして変換しテキストボックスに表示させます。こちらはServerサイドの逆バージョンなのであまり特筆して書くことはないかなぁと思いますので、コードだけ載せておきます。

MainWindow.xaml
<Window x:Class="AppsCommunicationClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Client" Height="160" Width="240">
    <Grid>
        <TextBox x:Name="value" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" HorizontalAlignment="Left" Height="127" TextWrapping="Wrap" VerticalAlignment="Top" Width="229" FontSize="48" Text="{Binding Value}"/>
    </Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.ComponentModel;

namespace AppsCommunicationClient
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new Client();
        }
    }

    public class Client : INotifyPropertyChanged
    {
        public System.Net.Sockets.NetworkStream Stream { get; set; }

        private int _value;

        public int Value
        {
            get
            {
                return _value;
            }

            set
            {
                _value = value;
                OnPropertyChanged("Value");
            }
        }

        public Client()
        {
            var _client = new System.Net.Sockets.TcpClient("localhost", 12301);

            Stream = _client.GetStream();

            this.Value = 0;

            read_value();
        }

        ~Client()
        {
            Stream.Close();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        async void read_value()
        {
            var enc = System.Text.Encoding.UTF8;
            var send_bytes = enc.GetBytes("read"); // 現状送る文字はなんでもよい
            byte[] result_byte = new byte[256];

            Stream.Write(send_bytes, 0, send_bytes.Length);

            await Stream.ReadAsync(result_byte, 0, result_byte.Length);

            Value = int.Parse(enc.GetString(result_byte));

            read_value();
        }
    }
}

参考資料

いくつかは記事中にリンクを張っていますが、以下のサイトを参考にしました。

補足

データバインディング

WPFでは、新しい機能の中の一つに「データバインディング」というものがあり、オブジェクトのプロパティ同士をリンクさせる(バインディングさせる)ことができる機能です。

例えば、スライドバーを移動させると、テキストの数値が変化するようなアプリを作る場合、データバインディングを使うことで簡単に書けます。

<Window x:Class="WpfApplication5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Slider x:Name="slider1" Value="0" Margin="93,119,122,145"/>
        <TextBlock Text="{Binding Value, ElementName=slider1}" Margin="93,194,131,99"/>
    </Grid>
</Window>

上記のように、SliderValueプロパティを TextBlockTextプロパティにバインディングすることにより、スライダーを動かすと、テキストもリアルタイムで更新されます。

slider.png

バインディングの記述はXAMLだけでなく、C#側でも記述が可能です。先の例では、スライダーを動かしてテキストの値を動かしました。今度は、テキスト側の値を動かしてスライダーを動かしてみましょう。

それには __データコンテキスト(DataContext)__が便利です。

データコンテキスト

データコンテキストは、UIオブジェクトではないオブジェクトをバインディング対象とする仕組みで、先の例でElementNameslider1を指定していましたが、データコンテキストを指定することで、ElementNameがデフォルトでデータコンテキストのオブジェクトに指定されます。以下に例を示します。

MainWindow.xaml.cs
using System.Windows;

namespace WpfApplication5
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // MainWindow の データコンテキストプロパティに
            // ValueObjectオブジェクトを指定
            // ValueObjectはValueプロパティを持っている
            this.DataContext = new ValueObject();
        }
    }

    public class ValueObject
    {
        // double にすることでスライダーが滑らかに移動可能
        // テキストボックスはdouble値が表示される。
        // int にするとスライダーは整数値の位置にのみ移動可能
        // テキストボックスにはint値が表示される。
        public double Value { get; set; }

        public ValueObject()
        {
            Value = 0.0;
        }
    }
}
MainWindow.xaml
<Window x:Class="WpfApplication5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="156" Width="386">
    <Grid>
        <!-- Slider の Value プロパティ、TextBox の Text プロパティに Value をバインディング -->
        <Slider x:Name="slider1" Value="{Binding Value}" Margin="0,26,0,-26"/>
        <TextBox Text="{Binding Value}" Margin="97,80,93,10" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
    </Grid>
</Window>

上記のようにすると、Sliderの値がTextBoxに反映され、TextBoxの値がSliderに反映されるようになります。

ここまで書いといてなんですが、以下のURLを見れば大体わかるかと。
http://ufcpp.net/study/dotnet/wpf_binding.html

30
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?