プログラムの事とか

お約束ですが「掲載内容は私個人の見解です」

Xamarin.Formsでページの壁紙をイイ感じに表示したい

今日(2018/12/12)時点の最新のXamarin.Forms(not Preview)でのお話

例によって正しいかどうかはご自身で判断してください

  • 対象はiOS(Androidは確認してないから知らない)
  • Emulatorでのみ動作確認
  • Visual Studio 2017でXamarin.Formsのプロジェクト作ってそのテンプレートをいじって試してみる
  • いいかんじ = アスペクト比固定でFill

です

ContentPage.BackgroundImageに指定

375 x 812ピクセルの壁紙をiPhone XSエミュレーター使って確認します

<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:BackgroundImageTest"
    x:Class="BackgroundImageTest.MainPage"
    BackgroundImage="bg375.jpg">

    <StackLayout>
        <!-- Place new controls here -->
        <Label Text="Welcome to Xamarin.Forms!" 
           HorizontalOptions="Center"
           VerticalOptions="CenterAndExpand" />
    </StackLayout>

</ContentPage>

結果

f:id:puni-o:20181212133641j:plain

なんかよさそうですね

壁紙のサイズを100 x 178ピクセルにして試す

<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:BackgroundImageTest"
    x:Class="BackgroundImageTest.MainPage"
    BackgroundImage="bg100.jpg">

以下略
</ContentPage>

結果

f:id:puni-o:20181212133656j:plain

はい

ということでContentPage.BackgroundImageはあきらめました (BackgroundImageを制御するものが見つからなかったので)

壁紙のサイズを端末のサイズに合わせればいいじゃんと思うかもしれませんが、Androidに限らずiPhoneも今やいろんなアスペクト比の端末があるんでダメですね

Imageを使う

ググると大体この方法ですね

<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:BackgroundImageTest"
    x:Class="BackgroundImageTest.MainPage">

    <Grid>
        <Image Aspect="AspectFill" Source="bg100.jpg"/>
        <StackLayout>
            <!-- Place new controls here -->
            <Label Text="Welcome to Xamarin.Forms!" 
           HorizontalOptions="Center"
           VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </Grid>
</ContentPage>

結果

f:id:puni-o:20181212134847j:plain

今度こそよさそう?

UseSafeAreaを指定する

iPhone Xが出てできた概念ですかね?どういうものかはUseSafeAreaでググればすぐに画像とかでてくると思います。このプロパティを有効にしておけばノッチとかその辺を除いたサイズでどうにかしてくれるってやつですね

<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:BackgroundImageTest"
    x:Class="BackgroundImageTest.MainPage"
    xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" 
    ios:Page.UseSafeArea="True"
    >

    <Grid>
        <Image Aspect="AspectFill" Source="bg375.jpg"/>
以下略

結果

f:id:puni-o:20181212140559j:plain

UseSafeAreaの効果がよくわかりますね (

ダメだけどContentPage.BackgroundImageで試してみる

<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:BackgroundImageTest"
    x:Class="BackgroundImageTest.MainPage"
    xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" 
    ios:Page.UseSafeArea="True"
    BackgroundImage="bg375.jpg"
    >
以下略

結果

f:id:puni-o:20181212140846j:plain

こっちでイキたいけどいイケないもどかしさ

ImageをContentの外側まで引き延ばす

これが現在の私の答えです

MainPage.xaml

<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:BackgroundImageTest"
    x:Class="BackgroundImageTest.MainPage"
    xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" 
    ios:Page.UseSafeArea="True"
    >

    <Grid>
        <Image x:Name="BgImage" Aspect="AspectFill" Source="bg375.jpg"/>
以下略

MainPage.xaml.cs

protected override void OnAppearing()
{
    base.OnAppearing();
    var inset = On<iOS>().SafeAreaInsets();
    BgImage.Margin = new Thickness(-inset.Left, -inset.Top, -inset.Right, -inset.Bottom);
}

結果

f:id:puni-o:20181212141349j:plain

他の端末でもいい感じに壁紙してくれるんじゃないでしょうか(多分)

Azure Functionsに排他制御を入れる

FunctionsでFunction単位で排他制御したいことがあったので調べてみました

結論はBLOB作ってそれをロックでした(他にあるのかしらない)

BLOBのロックは

Microsoft Azure Storage でのコンカレンシー制御の管理 | Microsoft Docs

ここに書いてあります

ということで実験しましょう

HttpTriggerなFunctionを作る

こんな感じ

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
        [Table("Log")] CloudTable logTable,
        ILogger log)
    {
        string name = req.Query["name"];

        for (var i = 0; i < 5; i++)
        {
            await logTable.ExecuteAsync(TableOperation.Insert(new LogEntity(name, i)));
            await Task.Delay(1000);
        }

        return new OkResult();
    }

    public class LogEntity : TableEntity
    {
        public string Name { get; set; }
        public int Num { get; set; }
        public LogEntity()
        {
            PartitionKey = "Log";
            RowKey = Guid.NewGuid().ToString();
        }
        public LogEntity(string name, int num) : this()
        {
            Name = name;
            Num = num;
        }
    }
}

テンプレートをいじってテーブルストレージ(Log)にデータを保存します

同時制御したいので書き込んで1秒待って、を5回繰り返します

あとはデバッグ実行して

http://localhost:7071/api/Function1?name=a
http://localhost:7071/api/Function1?name=b

で呼び出し

書き込んだ値を時間でソートすると

f:id:puni-o:20181130184226j:plain

二つのリクエストが同時に走っていることがわかると思います

排他制御を入れて順番に実行する

Run関数にBLOBのロック処理を追加します

public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    [Table("Log")] CloudTable logTable,
    Binder binder,
    ILogger log)
{
    string name = req.Query["name"];

    // BLOBをロックすることで排他制御とする
   #region リース取得
    var lockBlob = await binder.BindAsync<CloudBlockBlob>(new BlobAttribute("temp/lock"));
    if (!await lockBlob.ExistsAsync()) await lockBlob.UploadTextAsync("lock");  // 無ければ作っているけど事前に作っておいた方がいいと思う
    var lease = "";
    for (var i = 0; i < 100; i++)   // 20秒挑戦してダメなら終わろう
    {
        try
        {
            lease = await lockBlob.AcquireLeaseAsync(TimeSpan.FromSeconds(15), null);
            Console.WriteLine($"blob lease = {lease}");
            break;
        }
        catch (StorageException storageException)
        {
            if (storageException.RequestInformation.HttpStatusCode == (int)HttpStatusCode.Conflict)
            {
                Console.WriteLine("Wait for release lease...");
                await Task.Delay(200);
                continue;
            }
            return new ObjectResult("Error") { StatusCode = (int)HttpStatusCode.Conflict };
        }
    }

    if (string.IsNullOrEmpty(lease))
    {
        // 取れなかったらエラーで終わる
        log.LogWarning("Can't get lease.");
        return new ObjectResult("Error") { StatusCode = (int)HttpStatusCode.Conflict };
    }
    var accessCondition = AccessCondition.GenerateLeaseCondition(lease);
   #endregion

    for (var i = 0; i < 5; i++)
    {
        await logTable.ExecuteAsync(TableOperation.Insert(new LogEntity(name, i)));
        await Task.Delay(1000);
    }

    // リース解放
    await lockBlob.ReleaseLeaseAsync(accessCondition);

    return new OkResult();
}

実行結果

f:id:puni-o:20181130184616j:plain

順番に実行されるようになりました

注意点とか

コピペすると行数が膨らんで楽しい

BLOBのリース取得部分をクラスにしてもっと簡単に使えるようにしていた人がいました。探せば出てくると思うよ

リース期間に注意

AcquireLeaseAsyncに渡せるのは確か15~60秒だったはずなので注意

リース取得のリトライに注意

Functionsはデフォルトで5分長くても10分動き続けると強制的に落とされるので(Consumptionプラン時だけ?)何も考えずにロックしまくると大変だよ

結論

排他制御はやればできる

けどやらないに越したことはない

フォルダ名の最後がスペースのフォルダを作る (作れない)

作れません

知っている人は当たり前だと思うかもしれませんが、そんな名前のフォルダ作ろうなんて思ったこともなかったので知らなかったよ

試してみる

Explorer

f:id:puni-o:20181108183446j:plain

こうすると

f:id:puni-o:20181108183458j:plain

こうなります。勝手に最後のスペースが消されます。警告とかそーいうの出ません

勝手に省略しないで

f:id:puni-o:20181108183517j:plain

こっちの仲間に入れてあげればいいのに

C#でやってみる

// 最後がスペースのパス
var path = @"g:\test ";
try
{
    Directory.CreateDirectory(path);  // これは正常終了する
}
catch
{
    Console.WriteLine("しっぱい");  // 例外とか吐かない
}

if (Directory.Exists(path))  // これもtrueが返ってくる
{
    var file = Path.Combine(path, "a.txt");  // あるはずフォルダの下にテキストを書いてみる
    try
    {
        File.WriteAllText(file, "にゃーん");
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);  // 「パス 'g:\test \a.txt' の一部が見つかりませんでした。」
    }
}

Oh...

せめてDirectory.Exists()falseを返してほしいんだけど・・なんか歴史的な経緯とかあるのかな~