yamamoWorks

.NET技術を中心に気まぐれに更新していきます

MediaKey Timer

皆さんSpotifyという音楽配信サービスはご存知ですか? ジャンル&気分というプレイリストがあり作業時や就寝時に音楽を流すのに最適です。 www.spotify.com f:id:yamamoWorks:20170103232900p:plain:w400

ただ少し使い勝手が悪く感じる事があります。 例えばSleepジャンルにあるこのプレイリストは全部で7時間30分の音楽が登録されており、就寝時に再生したら起きるまで音が鳴りっぱなしになってしまいます。 Spotiryのアプリに再生停止タイマーなる機能はありません。 f:id:yamamoWorks:20170103233001p:plain:w400

SpotifyをiPhoneで再生する場合はiPhone付属のタイマーアプリでタイマー終了時の動作を「再生停止」にセットすれば任意の時間で再生を停止する事ができます。 f:id:yamamoWorks:20170103235347p:plain:w300

これと同じ事をWindowsでも実現するアプリを作ってみました。

MediaKey Timer

昨今のキーボードにはメディア再生停止ボタンがついているものが多いかと思います。 f:id:yamamoWorks:20170103235946j:plain

SpotifyのWindowsアプリはこのメディア再生停止ボタンに対応しているので、これを指定した時間後に擬似的に押す処理を実行するだけのアプリです。 なおiTunesなどメディア再生停止ボタンに対応しているアプリであればコントロール可能です。

f:id:yamamoWorks:20170104000122p:plain

ダウンロードはこちら(ZIPを解凍して適当な場所に置いてください) github.com

Visual Studioスタートページの「最近使用したファイル」を自動で整理

昔から気になっていたのです、Visual Studioのスタートページにある「最近使用したファイル」にゴミが残ることを。

コードの動作確認などで一時的なプロジェクトを作っては消すなんてことは皆さん多々あると思うのですが、「最近使用したファイル」は既に存在しないプロジェクトも表示されてしまうんですよね。

そこで、勉強も兼ねて「最近使用したファイル」から存在しないエントリーを削除するVS拡張を作って公開してみました。

visualstudiogallery.msdn.microsoft.com

Visual Studio 2015の拡張機能と更新プログラムからもインストール可能です。 f:id:yamamoWorks:20161229220403p:plain

これをインストールしておくと「最近使用したファイル」から存在しないエントリーを自動で削除してくれます。 f:id:yamamoWorks:20161229220645p:plain

解説

折角ですので仕組みも解説しておきます。 まず「最近使用したファイル」はレジストリに記録されています。

HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\14.0\MRUItems\{a9c4a31f-f9cb-47a9-abc0-49ce82d0b3ac}\Items

VS拡張でここに記録されているソリューションファイル/プロジェクトファイルの存在確認をしてレジストリを書き換えればいいので朝飯前ですね。

まずExtensibilityにあるVSIX Projectプロジェクトを作成します。 f:id:yamamoWorks:20161229222143p:plain

次にExtensibilityにあるVisual Studio Packageをプロジェクトに追加します。 f:id:yamamoWorks:20161229222557p:plain

するとPackageクラスを継承したクラスが自動生成されているのでInitializeメソッドをオーバライドで実装します。

Registry

PackageクラスにはUserRegistryRootプロパティがあり「HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\14.0」を表すRegistryKeyを返してくれるので便利です。後は目的のサブキーを開いて書き換えるだけです。

protected override void Initialize()
{
    base.Initialize();

    var regKey = UserRegistryRoot.OpenSubKey("MRUItems")?
        .OpenSubKey("{a9c4a31f-f9cb-47a9-abc0-49ce82d0b3ac}")?
        .OpenSubKey("Items", true);

    if (regKey == null)
        return;

    var mruItems = regKey.GetValueNames()
        .Select(name => (string)regKey.GetValue(name))
        .Where(value => File.Exists(value.Split('|').First()))
        .ToArray();

    foreach (var name in regKey.GetValueNames())
    {
        regKey.DeleteValue(name);
    }

    for (var i = 0; i < mruItems.Length; i++)
    {
        regKey.SetValue(i.ToString(), mruItems[i]);
    }

    regKey.Close();
}

デバック実行すると別のVisual Studioが起動して動作確認が可能です。 ちなみに、ここで起動してくるVisual Studioが参照するレジストリは「... \VisualStudio\14.0Exp\ ...」となっており開発環境と混在しないようになっています。

で、上記コードで動かしてみると・・・少し残念な挙動となります。

実は「最近使用したファイル」に表示する内容はVS拡張のInitializeメソッドが呼ばれる前に既に読み込まれており 、Initializeメソッドでレジストリを変更してもその時の起動では反映されないのです。 もちろん、次に起動した際には反映された状態になります。

この挙動では納得がいきません (・へ・)

そこで「最近使用したファイル」が表示される仕組みをILSpyで探り、何とか起動時に反映されるように実現できました。

ProjectMruList

「最近使用したファイル」の処理で中心となるのが Microsoft.VisualStudio.Shell.UI.Internal.dll にある Microsoft.VisualStudio.PlatformUI.ProjectMruList というInternalのクラスです。
Internalです・・・黒魔術決定。

ProjectMruListのインスタンスはDataSourceFactoryから{9099ad98-3136-4aca-a9ac-7eeeaee51dca}のIDで取り出せる事が解ったので、下記のコードで実現できました。

protected override void Initialize()
{
    base.Initialize();

    var dataSourceFactory = GetService(typeof(SVsDataSourceFactory)) as IVsDataSourceFactory;
    if (dataSourceFactory == null)
        return;

    IVsUIDataSource projectMruList;
    dataSourceFactory.GetDataSource(Guid.Parse("9099ad98-3136-4aca-a9ac-7eeeaee51dca"), 1, out projectMruList);

    var projectMruListType = Type.GetType("Microsoft.VisualStudio.PlatformUI.ProjectMruList, Microsoft.VisualStudio.Shell.UI.Internal", true);
    var removeAtItemMethod = projectMruListType.GetMethod("RemoveItemAt");
    var itemsProperty = projectMruListType.GetProperty("Items");

    var fileSystemMruItemType = Type.GetType("Microsoft.VisualStudio.PlatformUI.FileSystemMruItem, Microsoft.VisualStudio.Shell.UI.Internal", true);
    var pathProperty = fileSystemMruItemType.GetProperty("Path");

    var items = (IList)itemsProperty.GetValue(projectMruList, null);
    for (var i = items.Count - 1; i > -1; i--)
    {
        var path = (string)pathProperty.GetValue(items[i], null);
        if (!File.Exists(path))
        {
            removeAtItemMethod.Invoke(projectMruList, new object[] { i });
        }
    }
}

朝飯前が晩飯前になってしまいました (;・∀・)

ソースコードはこちらに公開してあります。 github.com

Xamarin.Forms.Platform.WinForms

ひょんなことからXamarin.FormsをWindows Formsで動かす事に挑戦してみました。

ちゃんとした仕組みの解説は @atsushieno 大先生のブログを見て頂くとしまして、ここでは単に作成したコードを晒しておこうと思います。 qiita.com

実現出来ているのはLabelを表示するだけです。 レイアウトやプロパティ、イベント等には対応していません。 (StackLayoutとかどうやって実現してるんだろ…)

Platformの拡張にはXamarin.Forms.Coreの中にあるInternalなInterfaceの実装が必要なのでInternalsVisibleToを設定する為だけにXamarin.FormsのソースコードをForkする形にしています。 また、Xamarin.Formsを丸ごとビルドするのは辛いので別途ソリューションファイル Platform.WinForms.sln を用意しています。

正しい実装方法である確証は0%なのでご了承ください! (単なるネタです)

github.com

MSALをXamarin.Forms+Prismで使う

本エントリーは Xamarin Advent Calendar 2016 (その2) 22日目のエントリーです。

今回はMicrosoftの認証ライブラリ「Microsoft Authentication Library」のXamarin.Forms + Prismで構成されたモバイルアプリでの実装方法を紹介します。

Microsoft Authentication Library (MSAL)

以前からAzure ADをモバイルアプリケーションの認証で利用する為に Active Directory Authentication Library (ADAL) というライブラリが提供されています。
これに対し Microsoft Authentication Library (MSAL) はADALの後継とされるライブラリで、Azure ADの他にMicrosoftアカウントやAzure AD B2Cの認証も1つのプログラミングモデルでカバーするライブラリとしてBuild 2016で発表されました。もちろんXamarinからも使用可能なライブラリです?

Preparation

Azure側の登録作業は公式ブログの記事にある通りなので割愛します。

Authenticate Your Mobile Apps Using Microsoft Authentication Library

Design

Xamarinの公式ブログでもMSALの実装方法は紹介されているのですが、各プラットフォーム毎にPageRendererでログインページを実装しコードビハインドで実現する方法になっているので、ここではPrismのPlatformInitializerを用いた方法で実装したいと思います。
PlatformInitializerについてはnuitsさんのブログを参考にして頂くのが良いかと思います。 www.nuits.jp

MSALの使い方の概要は以下の通り。

  1. PublicClientApplicationのインスタンスを生成する
  2. PublicClientApplicationのインスタンスにPlatformParametersを設定する
  3. PublicClientApplication.AcquireTokenAsync()メソッドで認証を実行する

非常に簡単なのですが、重要なのが2つ目のPlatformParametersを設定する部分です。 これは名前の通り各プラットフォーム固有のパラメータを扱うもので、実態はMSALライブラリ側でWebページの認証画面を表示する為にiOSのUIViewControllerやAndroidのActivityを渡す仕組みになっています。

Xamarinの公式ブログの実装方法ではAppクラスにPublicClientApplicationのプロパティを用意し、カスタムレンダラーで作成した各プラットフォームのログインページからPlatformParametersを渡す方法となっていますが、今回はPCLプロジェクトにIPlatformParametersFactoryインターフェイスを定義し、各プラットフォームでPlatformParametersを返す処理を実装します。

Implementation

IPlatformParametersFactory

まずPCLプロジェクトにIPlatformParametersFactoryインターフェイスを以下のように定義します。

namespace MSALApp
{
    public interface IPlatformParametersFactory
    {
        IPlatformParameters GetParameters();
    }
}

次に各プラットフォームでIPlatformParametersFactoryを実装します。

Android

namespace MSALApp.Droid
{
    class PlatformParametersFactory : IPlatformParametersFactory
    {
        public IPlatformParameters GetParameters()
        {
            // Xamarinの公式ブログではPageRender.Context as Activityを渡している
            return new PlatformParameters(Forms.Context as Activity);
        }
    }
}

iOS

namespace MSALApp.iOS
{
    class PlatformParametersFactory : IPlatformParametersFactory
    {
        public IPlatformParameters GetParameters()
        {
            // Xamarinの公式ブログではPageRender自身を渡している
            return new PlatformParameters(UIApplication.SharedApplication.KeyWindow.RootViewController);
        }
    }
}

UWP

namespace MSALApp.UWP
{
    class PlatformParametersFactory : IPlatformParametersFactory
    {
        public IPlatformParameters GetParameters()
        {
            return new PlatformParameters();
        }
    }
}

IPlatformInitializer

次にPrism (Unity) に各プラットフォームのPlatformParametersFactoryを登録します。 Prismのプロジェクトテンプレートからプロジェクトを作成していればIPlatformInitializerを実装するクラスが各プラットフォームに自動生成されています。

  • Android : MainActivity.cs -> AndroidInitializer
  • iOS : AppDelegate.cs -> iOSInitializer
  • UWP : MainPage.xaml.cs -> UwpInitializer

RegisterTypesに記述するコードはどのプラットフォームも同じです。

public class AndroidInitializer : IPlatformInitializer
{
    public void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType(typeof(IPlatformParametersFactory), typeof(PlatformParametersFactory));
    }
}

View

LoginCommandをバインドしたログインボタンとユーザ情報を表示するラベルを配置しておきます。

<StackLayout Orientation="Vertical" Margin="20,40">
    <Button Text="Login" Command="{Binding LoginCommand}" />
    <Label Text="{Binding User.Name}" />
    <Label Text="{Binding User.DisplayableId}" />
</StackLayout>

ViewModel

ViewModelのコンストラクタ引数にIPlatformParametersFactoryインターフェイスのパラメータを定義し、各プラットフォームで実装したPlatformParametersFactoryを受け取れるようにします。

ここで注意しなければならないのはPublicClientApplicationにPlatformParametersをセットするタイミングです。ViewModelのコンストラクタでPublicClientApplicationのインスタンスを生成した時点でセットしたいところですが、このタイミングではまだUIApplication.SharedApplication.KeyWindow.RootViewControllerがnullなので、ログインボタンを押した処理の中でPlatformParametersFactoryから取得します。

namespace MSALApp.ViewModels
{
    public class MainPageViewModel : BindableBase
    {
        public DelegateCommand LoginCommand { get; }

        private User _user;
        public User User
        {
            get { return _user; }
            set { SetProperty(ref _user, value); }
        }

        public MainPageViewModel(IPlatformParametersFactory parametersFactory)
        {
            var client = new PublicClientApplication("your-app-id");

            LoginCommand = new DelegateCommand(
                async () =>
                {
                    if (client.PlatformParameters == null)
                    {
                        client.PlatformParameters = parametersFactory.GetParameters();
                    }

                    var result = await client.AcquireTokenAsync(new[] { "User.Read" });

                    User = result.User;
                });
        }
    }
}

Finish

これでMVVMなスッキリした実装となりました。

Android iOS UWP
f:id:yamamoWorks:20161218190544g:plain f:id:yamamoWorks:20161218190550g:plain f:id:yamamoWorks:20161218190554g:plain

Azure Mobile AppsのEasy TablesでAdd from CSVが出る際の回避方法

2016/11/19現在、以下の不具合は解消されているようです

Azure Mobile AppsのEasy Tablesですが、ここ数日(2016/11/11現在)の間に「Add from CSV」を実行すると下記のエラーが発生するようになってしまいました。
f:id:yamamoWorks:20161111223302p:plain

これはMobile Apps側の不具合のようです。
https://social.msdn.microsoft.com/Forums/en-US/c4a3a6fa-67de-444f-ba9d-cf0dfc0faaf9/easy-tables-add-from-csv-is-not-working?forum=azuremobile

回避策

暫定の回避策(バージョンを下げる)が提示されているので手順を説明します。

  1. 該当のMobile Appを選択
  2. Application settingsの中のApp settingsセクションを探す
  3. MobileAppsManagement_EXTENSION_VERSIONの「latest」を「1.0.367」に変更 ※2016/11/11現在
  4. Saveボタンを押す f:id:yamamoWorks:20161111223733p:plain

これで「Add from CSV」が利用できるようになります。