読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

WPFからUWPのAPIを使ってBLEの操作をしよう

WPF

さて、UWPのAPIを使えるということで先日アドバタイズパケットの受信をやりました。

blog.okazuki.jp

今度はBLEの通信をしてみたいと思います。

参考ページは以下。

blog.fenrir-inc.com

そして、今回使うセンサーはSensorTagのv1です。

www.tij.co.jp

Windows Runtime用ソースコードがあるので、参考にさせてもらいます。

sensortag.codeplex.com

作ってみよう

WPFのプロジェクトを作ってUwpDesktopパッケージをNuGetからインストールします。SensorTagで使うUUIDを定義します。

namespace BleSample
{
    public class SensorTagUuid
    {
        // https://sensortag.codeplex.com/SourceControl/latest#SensorTagLibrary/SensorTagLibrary/Source/SensorTagUuid.cs
        public const string UuidIrtService = "f000aa00-0451-4000-b000-000000000000";
        public const string UuidIrtData = "f000aa01-0451-4000-b000-000000000000";
        public const string UuidIrtConf = "f000aa02-0451-4000-b000-000000000000";
    }
}

画面に初期化用ボタンと値を読み込み始めるボタンとデータ表示用TextBlockを置きます。

<Window x:Class="BleSample.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:BleSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Button Content="Connect"
                Click="ButtonConnect_Click" />
        <Button Content="Read value"
                Click="ButtonReadValue_Click" />
        <TextBlock x:Name="TextBlockTemp" />
    </StackPanel>
</Window>

そして、コードビハインドにUWPのAPIを使ってさくっとコードを書きましょう。

using System;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Windows;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Storage.Streams;

namespace BleSample
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private GattDeviceService GattDeviceService { get; set; }

        private GattCharacteristic GattCharacteristic { get; set; }

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void ButtonConnect_Click(object sender, RoutedEventArgs e)
        {
            // SensorTagを取得
            var selector = GattDeviceService.GetDeviceSelectorFromUuid(new Guid(SensorTagUuid.UuidIrtService));
            var devices = await DeviceInformation.FindAllAsync(selector);
            var deviceInformation = devices.FirstOrDefault();
            if (deviceInformation == null)
            {
                MessageBox.Show("not found");
                return;
            }

            this.GattDeviceService = await GattDeviceService.FromIdAsync(deviceInformation.Id);
            MessageBox.Show($"found {deviceInformation.Id}");

            // センサーの有効化?
            var configCharacteristic = this.GattDeviceService.GetCharacteristics(new Guid(SensorTagUuid.UuidIrtConf)).First();
            var status = await configCharacteristic.WriteValueAsync(new byte[] { 1 }.AsBuffer());
            if (status == GattCommunicationStatus.Unreachable)
            {
                MessageBox.Show("Initialize failed");
                return;
            }
        }

        private async void ButtonReadValue_Click(object sender, RoutedEventArgs e)
        {
            if (this.GattDeviceService == null)
            {
                MessageBox.Show("Please click connect button");
                return;
            }

            // 値を読み始める
            if (this.GattCharacteristic == null)
            {
                this.GattCharacteristic = this.GattDeviceService.GetCharacteristics(new Guid(SensorTagUuid.UuidIrtData)).First();
                this.GattCharacteristic.ValueChanged += this.GattCharacteristic_ValueChanged;

                var status = await this.GattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                if (status == GattCommunicationStatus.Unreachable)
                {
                    MessageBox.Show("Failed");
                }
            }
        }

        private async void GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
        {
            // 値を読んで表示する
            await this.Dispatcher.InvokeAsync(() =>
            {
                var data = new byte[args.CharacteristicValue.Length];
                DataReader.FromBuffer(args.CharacteristicValue).ReadBytes(data);
                var temp = BitConverter.ToUInt16(data, 2) / 128.0;
                this.TextBlockTemp.Text = $"{temp}℃";
            });
        }
    }
}

基本的にGattなにがし系のクラスを使う感じですね。あとはIBufferとbyte[]の相互変換あたりがポイントでしょうか。あとはSensorTagのコードを参考に真似させてもらいました。

実行してボタンをぽちぽちっと押すと温度が表示されます。

f:id:okazuki:20160722225923p:plain

ソースコード

ソースコード全体はGitHubに上げておきます。

github.com

Office 365の機能をUWPから叩く方法

UWP

Office 365のAzure ADでアプリケーションを作成します。ネイティブアプリケーションとして作成しておきます。 他のアプリケーションに対するアクセス許可でOffice 365 Exchange Onlineを追加して適当なアクセス許可を与えておきます。

クライアントIDを控えて準備は完了です。

UWPのプロジェクトに以下のパッケージをNuGetから追加します。

  • Microsoft.IdentityModel.Clients.ActiveDirectory
  • Microsoft.Office365.OutlookServices

そして、ボタンクリックイベントハンドラあたりに以下のコードを追加するとOKです。

private async void ButtonSignIn_Click(object sender, RoutedEventArgs e)
{
    var authContext = new AuthenticationContext(
        "https://login.microsoftonline.com/common");
    this.Client = new OutlookServicesClient(
        new Uri("https://outlook.office365.com/api/v1.0"),
        async () =>
        {
            var settings = ApplicationData.Current.LocalSettings;
            if (settings.Values.ContainsKey("ExpiresOn"))
            {
                var expiresOn = (DateTimeOffset)settings.Values["ExpiresOn"];
                if (expiresOn >= DateTimeOffset.Now)
                {
                    return (string)settings.Values["AccessToken"];
                }
            }
            try
            {
                var ar = await authContext.AcquireTokenAsync(
                    "https://outlook.office365.com",
                    "クライアントID",
                    new Uri("http://uwp.okazuki.jp"),
                    new PlatformParameters(PromptBehavior.Auto, false));
                settings.Values["ExpiresOn"] = ar.ExpiresOn;
                settings.Values["AccessToken"] = ar.AccessToken;
                return ar.AccessToken;
            }
            catch (AdalException ex)
            {
                Debug.WriteLine(ex);
                return null;
            }
        });
    try
    {
        this.TextBlock.Text = (await this.Client.Me.ExecuteAsync()).DisplayName;
    }
    catch (DataServiceQueryException ex)
    {
        Debug.WriteLine(ex);
        this.TextBlock.Text = ex.Message;
    }
}

ここでは表示名をとってきています。ちゃんと認証ができたかを確認するのと確実に認証ダイアログを出すために、最初は、こんな感じでユーザー名とかとってきておくのがいいかもしれません。

二度目以降は有効期限が切れるまでアクセストークンを使いまわすようにしています。ただ、有効期限を見ると1時間程度なので、この処理はいらないかもしれないですね…。

ASP.NET(Azure WebApp)でMicrosoft.Office365.OutlookServicesを使って予定をとってくる

Azure Office365 ASP.NET

超苦労したのでメモっておきます。

まず、Office 365のテナントからいけるAzure ADにアプリケーションを追加します。追加したら、CliendIDとClientSecret(期限つきのキーのやつ)とTenantID(エンドポイントを表示したときに見れるやつ)をとっておきます。

次に、ASP.NET MVCのプロジェクトに以下のライブラリを追加します。

  • Microsoft.IdentityModel.Clients.ActiveDirectory
    • 2016/7/22現在、最新版をとってくるとエラーになるので3.10.xを入れること!(はまったポイント)
  • Microsoft.Office365.Discovery
  • Microsoft.Office365.OutlookServices
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Security.OpenIdConnect

そして、OWINのStartupクラスを作ってAzure ADとの連携に必要な決まり文句を書きます。

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IdentityModel.Claims;
using System.Web;
using OwinSecurityError.Models;

[assembly: OwinStartup(typeof(OwinSecurityError.Startup))]

namespace OwinSecurityError
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = Consts.ClientId,
                    ClientSecret = Consts.ClientSecret,
                    Authority = "https://login.windows.net/O365のテナント名.onmicrosoft.com/",
                    Notifications = new OpenIdConnectAuthenticationNotifications()
                    {
                        //
                        // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.

                        AuthorizationCodeReceived = async (context) =>
                        {
                            var code = context.Code;

                            var credential = new ClientCredential(Consts.ClientId, Consts.ClientSecret);
                            var userObjectId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

                            var authContext = new AuthenticationContext(Consts.Authority, new ADALTokenCache(userObjectId));

                            await authContext.AcquireTokenByAuthorizationCodeAsync(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, "https://graph.windows.net");
                        },


                        RedirectToIdentityProvider = (context) =>
                        {
                            // This ensures that the address used for sign in and sign out is picked up dynamically from the request
                            // this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
                            // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
                            string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
                            context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
                            context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;

                            return Task.FromResult(0);
                        },

                        AuthenticationFailed = (context) =>
                        {
                            // Suppress the exception if you don't want to see the error
                            context.HandleResponse();

                            return Task.FromResult(0);
                        }

                    },
                });
        }
    }

    static class Consts
    {
        public static string ClientId { get; } = "クライアントID";

        public static string ClientSecret { get; } = "クライアントシークレット";

        public static string Authority { get; } = "https://login.windows.net/テナントID";
    }
}

ADALTokenCacheがポイントで、こいつでTokenをキャッシュしておきます。正しい実装はデータベースとかに値をキャッシュするのですが、今回はサンプルなのでインメモリで保持してます(再デプロイとかで消えるので実運用ではだめよ)

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace OwinSecurityError.Models
{
    public class ADALTokenCache : TokenCache
    {
        private static Dictionary<string, byte[]> Cache { get; } = new Dictionary<string, byte[]>();

        private string User { get; }

        public ADALTokenCache(string user)
        {
            this.User = user;
            this.BeforeAccess = this.BeforeAccessNotification;
            this.AfterAccess = this.AfterAccessNotification;
            this.BeforeWrite = this.BeforeWriteNotification;

            this.DeserializeIfContains();
        }

        private void BeforeWriteNotification(TokenCacheNotificationArgs args)
        {
        }

        private void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            lock (Cache)
            {
                Cache[this.User] = this.Serialize();
            }
        }

        private void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            this.DeserializeIfContains();
        }

        private void DeserializeIfContains()
        {
            lock (Cache)
            {
                if (Cache.ContainsKey(this.User))
                {
                    this.Deserialize(Cache[this.User]);
                }
            }
        }
    }
}

あとは、コントローラあたりでOutlookServiceClientあたりを作ってやればOKです。この作り方も曲者で認証情報やらをこねくり回す必要があります。コードはこんな感じ。

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Office365.Discovery;
using Microsoft.Office365.OutlookServices;
using OwinSecurityError.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

namespace OwinSecurityError.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        public async Task<ActionResult> Index()
        {
            var client = await EnsureOutlookServicesClientCreatedAsync("Calendar");
            var user = await client.Me.ExecuteAsync();
            var date = DateTimeOffset.UtcNow - TimeSpan.FromDays(1);
            var view = client.Me.Calendar.GetCalendarView(date, date + TimeSpan.FromDays(2));
            var pages = await view.ExecuteAsync();
            var result = new List<string>();
            while (true)
            {
                result.AddRange(pages.CurrentPage.Select(x => $"{x.Location.DisplayName} {x.Subject}"));
                if (!pages.MorePagesAvailable)
                {
                    break;
                }
                await pages.GetNextPageAsync();
            }
            this.ViewBag.Title = user.DisplayName;
            return View(result);
        }

        private static async Task<OutlookServicesClient> EnsureOutlookServicesClientCreatedAsync(string capabilityName)
        {

            var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            var userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

            AuthenticationContext authContext = new AuthenticationContext(Consts.Authority, new ADALTokenCache(signInUserId));
            try
            {
                var discClient = new DiscoveryClient(new Uri("https://api.office.com/discovery/v1.0/me/"),
                    async () =>
                    {
                        var authResult = await authContext.AcquireTokenSilentAsync("https://api.office.com/discovery/",
                                                                                   new ClientCredential(Consts.ClientId,
                                                                                                        Consts.ClientSecret),
                                                                                   new UserIdentifier(userObjectId,
                                                                                                      UserIdentifierType.UniqueId));

                        return authResult.AccessToken;
                    });

                var dcr = await discClient.DiscoverCapabilityAsync(capabilityName);

                return new OutlookServicesClient(dcr.ServiceEndpointUri,
                    async () =>
                    {
                        var authResult = await authContext.AcquireTokenSilentAsync(dcr.ServiceResourceId,
                                                                                   new ClientCredential(Consts.ClientId,
                                                                                                        Consts.ClientSecret),
                                                                                   new UserIdentifier(userObjectId,
                                                                                                      UserIdentifierType.UniqueId));

                        return authResult.AccessToken;
                    });
            }
            catch (AdalException ex)
            {
                if (ex.ErrorCode == AdalError.FailedToAcquireTokenSilently)
                {
                    authContext.TokenCache.Clear();
                    throw;
                }
                return null;
            }
        }
    }
}

クライアントを作ったら、後はAPIはそれっぽくいけるのでCalendarから値をとったりしてます。

こんな感じのViewを定義すればリストで会議室と予定が表示されます。

@model List<string>
@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<ul>
    @foreach (var item in this.Model)
    {
        <li>@item</li>
    }
</ul>

超苦労した!

ReactiveProperty v2.9.0とv3.0.0-pre5をリリースしました。

ReactiveProperty

id:neueccさんがプルリクをくれました。最近多いですね!

www.nuget.org

今回追加された機能は、AsyncReactiveCommandクラスになります。Subscribeメソッドが非同期メソッドを受け取って、その非同期メソッドが実行中の間は自動的にCanExecuteをFalseにしてくれるというCommandです。今まで頑張って自前で実行中かどうかを判断してたのがいらなくなるんですね。はい。

こんな感じのViewModelが作れます。

using Reactive.Bindings;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sample.ViewModels
{
    public class AsyncReactiveCommandViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public AsyncReactiveCommand ShareSourceCommand1 { get; }

        public AsyncReactiveCommand ShareSourceCommand2 { get; }

        private ReactiveProperty<bool> ShareSource { get; } = new ReactiveProperty<bool>(true);

        public AsyncReactiveCommandViewModel()
        {
            this.HeavyProcessCommand = new AsyncReactiveCommand();
            this.HeavyProcessCommand.Subscribe(async _ => await Task.Delay(3000));

            this.ShareSourceCommand1 = this.ShareSource.ToAsyncReactiveCommand();
            this.ShareSourceCommand1.Subscribe(async _ => await Task.Delay(3000));
            this.ShareSourceCommand2 = this.ShareSource.ToAsyncReactiveCommand();
            this.ShareSourceCommand2.Subscribe(async _ => await Task.Delay(3000));
        }
    }
}

今回はDelayを使ってますが、ここで本来重たい処理を書きます。ShareSourceCommandのほうは、同じReactivePropertyを共有することで、どちらかが実行中のときは、もう片方も実行できなくなるという便利な感じに動いてくれます。

BLEのアドバタイズパケット受信イベントの引数のBluetoothLEAdvertisementReceivedEventArgsからMACアドレスを取得する方法

UWP

argsがBluetoothLEAdvertisementReceivedEventArgsとしたとき、以下のコードで取得できます。

string.Join(":", BitConverter.GetBytes(args.BluetoothAddress).Reverse().Select(x => x.ToString("X2"))).Substring(6);

以下のGitHubのコードに書いてありました。

github.com

WPFでBLEのアドバタイズパケットを拾ってみよう

WPF

UWPのAPIを使ってできるかやってみます。

まず、WpfBleSampleAppという名前でWPFアプリケーションを作成してNuGetからUwpDesktopをインストールします。こいつは、UWPを使うために必要な設定をやってくれる便利な奴です。

f:id:okazuki:20160719220359p:plain

次に、以下の記事を参考にアドバタイズパケットを拾うコードを書きます。

blog.okazuki.jp

まず、画面のXAMLです。TextBlockを置いて、BLEの受信を開始・停止するためのLoadedとClosedイベントを紐づけてます。

<Window x:Class="WpfBleSampleApp.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:WpfBleSampleApp"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="350"
        Width="525"
        Loaded="Window_Loaded"
        Closed="Window_Closed">
    <Grid>
        <TextBlock x:Name="TextBlockRSSI" />
    </Grid>
</Window>

コードビハインドは、ほぼほぼ上記Blog記事のコードのコピペになります。

using System;
using System.Linq;
using System.Windows;
using Windows.Devices.Bluetooth.Advertisement;

namespace WpfBleSampleApp
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private BluetoothLEAdvertisementWatcher watcher;

        public MainWindow()
        {
            InitializeComponent();
            this.watcher = new BluetoothLEAdvertisementWatcher();

            // CompanyIDとかDataでフィルタリングしたいとき
            //var md = new BluetoothLEManufacturerData();
            //// company id 0xFFFF (多分これ https://www.bluetooth.com/specifications/assigned-numbers/company-Identifiers)
            //md.CompanyId = 0xFFFF; 

            //// data 0x1234
            //var w = new DataWriter();
            //w.WriteUInt16(0x1234);
            //md.Data = w.DetachBuffer();
            //this.watcher.AdvertisementFilter.Advertisement.ManufacturerData.Add(md);

            // rssi >= -60のとき受信開始するっぽい
            this.watcher.SignalStrengthFilter.InRangeThresholdInDBm = -60;
            // rssi <= -65が2秒続いたら受信終わるっぽい
            this.watcher.SignalStrengthFilter.OutOfRangeThresholdInDBm = -65;
            this.watcher.SignalStrengthFilter.OutOfRangeTimeout = TimeSpan.FromMilliseconds(2000);
            this.watcher.Received += this.Watcher_Received;
        }

        private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            await this.Dispatcher.InvokeAsync(() =>
            {
                var md = args.Advertisement.ManufacturerData.FirstOrDefault();
                if (md != null)
                {
                    // ManufactureDataをもとにCompanyIDとったりできる
                }
                this.TextBlockRSSI.Text = $"{args.Timestamp:HH\\:mm\\:ss}, RSSI: {args.RawSignalStrengthInDBm}, Address: {args.BluetoothAddress.ToString("X")}, Type: {args.AdvertisementType}";
            });
        }

        private void Window_Closed(object sender, EventArgs e)
        {
            this.watcher.Stop();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            this.watcher.Start();
        }
    }
}

実行してみましょう。

f:id:okazuki:20160719221215p:plain

ちゃんと取れてますね。

コード

コードはGitHubに上げています。

github.com

WPFのBehaviorをStyleで使う方法

WPF XAML

昔書いた記事にコメントがついたので改めてやってみました。

blog.okazuki.jp

最近はGitHubがあるのでコードを共有するのが楽でいいですね。

こんなクラスを用意してやります。BehaviorをCloneして追加してやる感じです。Cloneして渡さないと同じBehaviorのインスタンスは、複数クラスにアタッチできないのがポイントです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interactivity;

namespace StyleBehaviorSampleApp
{
    public class StyleBehaviorCollection : FreezableCollection<Behavior>
    {

        public static readonly DependencyProperty StyleBehaviorsProperty =
            DependencyProperty.RegisterAttached(
                "StyleBehaviors", 
                typeof(StyleBehaviorCollection), 
                typeof(StyleBehaviorCollection), 
                new PropertyMetadata((sender, e) =>
                {
                    if (e.OldValue == e.NewValue) { return; }

                    var value = e.NewValue as StyleBehaviorCollection;
                    if (value == null) { return; }

                    var behaviors = Interaction.GetBehaviors(sender);
                    behaviors.Clear();
                    foreach (var b in value.Select(x => (Behavior)x.Clone()))
                    {
                        behaviors.Add(b);
                    }
                }));

        public static StyleBehaviorCollection GetStyleBehaviors(DependencyObject obj)
        {
            return (StyleBehaviorCollection)obj.GetValue(StyleBehaviorsProperty);
        }

        public static void SetStyleBehaviors(DependencyObject obj, StyleBehaviorCollection value)
        {
            obj.SetValue(StyleBehaviorsProperty, value);
        }

        protected override Freezable CreateInstanceCore()
        {
            return new StyleBehaviorCollection();
        }
    }
}

あとは、適当なBehaviorを作ってこんな感じで使います。

<Window x:Class="StyleBehaviorSampleApp.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:StyleBehaviorSampleApp"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Window.Resources>
        <Style x:Key="ButtonAlertStyle"
               TargetType="{x:Type Button}">
            <Setter Property="local:StyleBehaviorCollection.StyleBehaviors">
                <Setter.Value>
                    <local:StyleBehaviorCollection>
                        <local:AlertBehavior Message="Hello world" />
                    </local:StyleBehaviorCollection>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <StackPanel>
        <Button Content="Alert1"
                Style="{StaticResource ButtonAlertStyle}" />
        <Button Content="Alert2"
                Style="{StaticResource ButtonAlertStyle}" />
        <Button Content="Alert3"
                Style="{StaticResource ButtonAlertStyle}" />
    </StackPanel>
</Window>

ソースコード全体は以下からどうぞ。

github.com

PCLでHttpUtlitityのUrlEncodeとUrlDecodeを使いたい

C#

UriクラスのEscapeDataStringとUnescapeDataStringメソッドを使いましょう。

Console.WriteLine(HttpUtility.UrlEncode(s));
Console.WriteLine(Uri.EscapeDataString(s));

var encodedString = HttpUtility.UrlEncode(s);
Console.WriteLine(HttpUtility.UrlDecode(encodedString));
Console.WriteLine(Uri.UnescapeDataString(encodedString));

Prism.WpfでPopupWindowActionで表示した要素の中でRegionManagerを使った画面遷移がしたい

Prism WPF

というサンプルをGitHubにあげました。

github.com

結構RegionBehaviorを使ったりBehaviorを使ったり頑張ってます。 ここら辺が現実的な妥協点かもしれませんと思います。

WPFでPrismをライトウェイトに使いたい「Hello world」

Prism WPF

WPF版Prismは使いこなすと強力です。でも使いこなすの大変です。ハイ。学習コストかけてられないし、学習コストかけたからといって1個のアプリ開発で、そのコストを回収できるかもわかりませんですしね。 ということで、なるべくライトにPrismを使ってみたいと思います。

Prismで使いたい機能

以下の機能を使おうと思います。

  • MVVM基本クラス
    • BindableBaseクラス
    • DelegateCommandクラス
  • ViewModelLocator
  • InteractionRequest
    • PopupWindowAction
  • DIコンテナのUnity

逆に以下の機能は使わない前提です。

  • Bootstrapper
  • Module
  • Region

では行ってみましょう。

プロジェクトの作成

LightweightPrismSampleという名前でプロジェクトを作ったという前提で説明します。NuGetからPrism.Unityをインストールします。

MainWindowは後で作り直すので削除しておきます。

Appクラス

App.xamlからStartupUriを消してStartupメソッドに書き換えます。ここでちょっとUnityの初期化とか書くようにします。

<Application x:Class="LightweightPrismSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:LightweightPrismSample"
             Startup="Application_Startup">
    <Application.Resources>
         
    </Application.Resources>
</Application>

ViewModelLocatorを使うためのおまじないみたいなものです。アプリで管理しているUnityのコンテナからViewModelを生成するようにしています。

using Microsoft.Practices.Unity;
using Prism.Mvvm;
using System.Windows;

namespace LightweightPrismSample
{
    public partial class App : Application
    {
        // アプリで管理するコンテナ
        private IUnityContainer Container { get; } = new UnityContainer();

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            ViewModelLocationProvider.SetDefaultViewModelFactory(x => this.Container.Resolve(x));
        }
    }
}

Viewの作成

Views名前空間にMainWindowを作成します。ViewModelLocatorを使うおまじないをAppクラスでしてるので、ViewModelLocatorが使えます。

<Window x:Class="LightweightPrismSample.Views.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:LightweightPrismSample.Views"
        xmlns:Prism="http://prismlibrary.com/"
        Prism:ViewModelLocator.AutoWireViewModel="True"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="300"
        Width="300">
    <Grid>
    </Grid>
</Window>

Appクラス再び

MainWindowを表示させます。せっかくなのでUnityから作りましょう(深い意味はない。DIしたければできるようになるっていうくらい)

using Microsoft.Practices.Unity;
using Prism.Mvvm;
using System.Windows;

namespace LightweightPrismSample
{
    public partial class App : Application
    {
        // アプリで管理するコンテナ
        private IUnityContainer Container { get; } = new UnityContainer();

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            ViewModelLocationProvider.SetDefaultViewModelFactory(x => this.Container.Resolve(x));
            this.Container.Resolve<MainWindow>().Show();
        }
    }
}

この時点で、MainWindowが表示されるようになります。

f:id:okazuki:20160716220307p:plain

ViewModelの作成

では、ViewModelを作成していきます。ViewModels名前空間に以下のようなViewModelを作ります。InteractionRequestやDelegateCommandやBindableBaseを使います。

using Prism.Commands;
using Prism.Interactivity.InteractionRequest;
using Prism.Mvvm;

namespace LightweightPrismSample.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        public DelegateCommand AlertCommand { get; }

        public InteractionRequest<INotification> AlertRequest { get; } = new InteractionRequest<INotification>();

        private string input;

        public string Input
        {
            get { return this.input; }
            set { this.SetProperty(ref this.input, value); }
        }

        public MainWindowViewModel()
        {
            this.AlertCommand = new DelegateCommand(() =>
                this.AlertRequest.Raise(new Notification { Title = "Alert", Content = this.Input }));
        }
    }
}

Viewの調整

ViewModelに合わせてViewも調整します。InteractionRequestに応答するトリガーや、PopupWindowActionなんかを使っています。Behaviorを使うのでSystem.Windows.Interactionアセンブリを参照に追加するのを忘れずに。

<Window 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:LightweightPrismSample.Views"
        xmlns:Prism="http://prismlibrary.com/"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        x:Class="LightweightPrismSample.Views.MainWindow"
        Prism:ViewModelLocator.AutoWireViewModel="True"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="300"
        Width="300">
    <i:Interaction.Triggers>
        <Prism:InteractionRequestTrigger SourceObject="{Binding AlertRequest}">
            <Prism:PopupWindowAction />
        </Prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    <StackPanel>
        <TextBox Text="{Binding Input, Mode=TwoWay}" />
        <Button Content="Alert"
                Command="{Binding AlertCommand}" />
    </StackPanel>
</Window>

実行

実行するとWindowが立ち上がり、TextBoxに入力した内容がボタンを押すとポップアップで表示されます。 画面遷移とかしたかったら自力でファイト!という感じで凝ったことしようとするとPrismの機能が恋しくなるかもしれませんが、そうじゃない軽いアプリならこんな構成もありかなという感じです。

f:id:okazuki:20160716221254p:plain

おまけ

今回のプロジェクトはGitHubに公開しています。

github.com

このプロジェクトを育てるには

AppクラスのStartupイベントでUnityのコンテナにModelやら必要なクラスの登録処理を書いたり色々やってよしなにやる感じです。