WPFでグリッド状に写真を並べる

写真などを扱うアプリケーションを作成していると、エクスプローラーにあるような選択可能な写真プレビュー画面が作りたくなることがあります。

f:id:laicos:20180115235613p:plain

これをWPFバインディング(ReactiveProperty)を使って実現します。

選択可能なリスト

ListViewを使います。ただし、要素は画像をグリッド状に並べたいので以下のカスタマイズをします。 またSelectionMode="Extended"を指定しておくとCTRL+クリックで複数選択可能になるような挙動になります。他にもSingle,Multipleが指定できます。

PhotosにはViewModelでImageSourceに当たるものを指定します。WPFのImageSourceは結構便利で画像のパス、URL、BitmapSourceなどを指定するだけで勝手に表示してくれます。

class Photo {
    public string Title { get; set; }
    public string Url{ get; set; }
}
<ListView ItemsSource="{Binding Photos}" SelectionMode="Extended" ScrollViewer.HorizontalScrollBarVisibility="Disabled"></ListView>

画像をグリッド状に並べる

WrapPanelを使います。ListView.ItemsPanelに指定することでListViewの親要素にすることができます。 また、横方向のスクロールを無効化しておくと、横幅に応じて画像配置の列数を変えてくれます。

<ListView ItemsSource="{Binding Photos}" SelectionMode="Extended" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel/>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>

子要素にUIを当てる

これはListView,ItemTemplateに指定します。ここさえ工夫すれば画像以外であっても任意のUIに割り当てることができます。 Bindingにはプロパティ名を入れると参照可能です

<ListView ItemsSource="{Binding Photos}" SelectionMode="Extended" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel/>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid Width="300" Height="200">
                <Image Source="{Binding Title}"></Image>
                <TextBlock Text="{Binding Url}"></TextBlock>
            </Grid>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

これでPhotosの内容によって画像グリッドを作成することができます。

C#でConoHa オブジェクトストレージを操作する

表題の通り、C# HttpClientからOpenStack Swiftの最低限動作するドライバを書きました。(コンテナ作成、削除やオブジェクトの作成、削除、コピー等)

バックエンドやAWS Lambda、Azure Functionsで動かすことも見越して一応.NET Core向けのプロジェクトでビルドしています。(おそらくどちらでも動くと思います)

GitHub - laicos0/objectstoragesharp: OpenStack Object Storage Library https://developer.openstack.org/api-ref/object-store/ (for ConoHa https://www.conoha.jp/guide/objectstoragerestapi.php)

それにしてもHttpClient,ひどくよくわからない挙動ばかりだった。

C# でOpenStack Keystoneへアクセス

参考↓

www.conoha.jp

オブジェクトストレージにアクセスする前に、トークンの取得をC#でやってみます。

ConoHaはOpenStack準拠しているようですので、まずは認証をするためにOpenStack Keystoneの仕様を読んでリクエストを投げます。

OpenStack Docs: Welcome to keystone’s documentation!

HttpClientでJson POSTリクエストを生成

HttpClientは罠が多いですが以下のようなコードを書きます。ポイントはHttpClientをstaticで持つところです。以前のWebClientと比べると何か違和感がありますが...。

"Content-Type"に"application/json"を指定して結果をjsonで受け取れるようにすることを忘れないでおきます。

public static class WebExtension {
    private static HttpClient httpClient = new HttpClient();

    public static HttpClient AddHeader(this HttpClient client, string name, string value) {
        client.DefaultRequestHeaders.TryAddWithoutValidation(name, value);
        return client;
    }
    public static HttpClient AddAcceptHeader(this HttpClient client, string header = "application/json") => client.AddHeader("Accept", header);
    public static HttpClient AddContentHeader(this HttpClient client, string header = "application/json") => client.AddHeader("Content-Type", header);
    public static HttpClient AddAuthToken(this HttpClient client, string token) => (token != null) ? client.AddHeader("X-Auth-Token", token) : client;


    public static Task<HttpResponseMessage> Post(string url, string jsonStr, string authToken = null) {
        httpClient.AddContentHeader().AddAuthToken(authToken);

        var content = new StringContent(jsonStr, Encoding.UTF8, "application/json");
        return httpClient.PostAsync(url, content);
    }
    public static Task<HttpResponseMessage> Post(string url, object data, string authToken = null) => Post(url, JsonConvert.SerializeObject(data), authToken);
}

送るデータをクラスを作成せずに用意する

Newtonsoft.JsonのJObjectをdynamicにキャストして使用するとjavascriptのような記述ができます。

contentをJsonからクラスにデシリアライズすれば完了です。

public class KeyStone {
    public string AuthUrl { get; protected set; }

    protected KeyStone() { }
    public static async Task<KeyStone> Authenticate(string url, string tenant, string user, string pass) {
        var data = new JObject() as dynamic;
        data.auth = new JObject() as dynamic;
        data.auth.tenantName = tenant;
        data.auth.passwordCredentials = new JObject() as dynamic;
        data.auth.passwordCredentials.username = user;
        data.auth.passwordCredentials.password = pass;

        HttpResponseMessage result = await WebExtension.Post(url, data, authToken: null);
        var content = await result.Content.ReadAsStringAsync();
        if (result.StatusCode != System.Net.HttpStatusCode.OK) {
            throw new HttpRequestException(result.ToString());
        }

        return new KeyStone() {
            AuthUrl = url,
            // Token
        };
    }
}

テスト結果

VSでクラスにキャストしましたが以下のようなデータが帰ってくれば成功です。

TokenとEndpointの内容を使ってOpenStack準拠の他のサービスにアクセスできます。

public class Rootobject {
    public Access access { get; set; }
}

public class Access {
    public Token token { get; set; }
    public Servicecatalog[] serviceCatalog { get; set; }
    public User user { get; set; }
    public Metadata metadata { get; set; }
}

public class Token {
    public DateTime issued_at { get; set; }
    public DateTime expires { get; set; }
    public string id { get; set; }
    public Tenant tenant { get; set; }
    public string[] audit_ids { get; set; }
}

public class Tenant {
    public string domain_id { get; set; }
    public string description { get; set; }
    public bool enabled { get; set; }
    public string id { get; set; }
    public string name { get; set; }
}

public class User {
    public string username { get; set; }
    public object[] roles_links { get; set; }
    public string id { get; set; }
    public Role[] roles { get; set; }
    public string name { get; set; }
}

public class Role {
    public string name { get; set; }
}

public class Metadata {
    public int is_admin { get; set; }
    public string[] roles { get; set; }
}

public class Servicecatalog {
    public Endpoint[] endpoints { get; set; }
    public object[] endpoints_links { get; set; }
    public string type { get; set; }
    public string name { get; set; }
}

public class Endpoint {
    public string region { get; set; }
    public string publicURL { get; set; }
}