Open8

MAUIマイグレーションメモ

かむかむ

XAML

xmlns="http://xamarin.com/schemas/2014/forms"

xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
に一括置換

xmlns:sv="clr-namespace:AiForms.Renderers;assembly=SettingsView"

xmlns:sv="clr-namespace:AiForms.Settings;assembly=SettingsView"
に一括置換

xmlns:extra="clr-namespace:AiForms.Dialogs.Abstractions;assembly=AiForms.Dialogs"

xmlns:extra="clr-namespace:AiForms.Dialogs;assembly=AiForms.Maui.Dialogs"
に一括置換

C# コード

using Xamarin.Forms を消す

Color

定数がColorsに変わったのでColor.RedなどはColors.Redというふうに変換する

PlatformSpecific

usingを以下に変更

using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific or AndroidSpecific;
using Platform = Microsoft.Maui.Controls.PlatformConfiguration;

かむかむ

Android

Maii ColorをAndroid Colorに

ToPlatform()を使う。

Maui Colorを ColorStateListに変換

ToDefaultColorStateList();

かむかむ

iOS

Releaseビルドでこける

System.ExecutionEngineException: Attempting to JIT compile method
とかのエラーでこける場合はプロジェクトファイルのiOSリリースのプロパティグループに
<UseInterpreter>true</UseInterpreter>
を追加する。

色をUIColorに

ToUIColorが非推奨に代わりにToPlatform()を使う。

ImageSourceをUIImageに

以下の拡張メソッドがある

var result = await ImageSource.GetPlatformImageAsync(MauiContext);
var icon = result.Value; // UIImageを取り出す
かむかむ

Control

Colorプロパティ

Color値のプロパティに文字列(#FF00FFなど)をBindしても無効になる。
Forms時代は自動で変換してくれたがMAUIではしてくれないので別途Converterなどが必要。
Xaml上から直接入力した文字列やStaticResource/DynamicResourceで指定したものは有効。

BoxView

CornerRadiusはBackgroundColorを設定していると効かない。代わりにColorを設定する。

Frame

Androidは何かバグってる(サイズ計算がおかしい)ので代わりにBorderを使う方が良い。
がBorderはBorderで画像のクリップができない問題があるので画像使う場合はFrameを使うしか無い。

Border

Frameに代わる境界線用のコントロール。
ただFrameに書いたようなクリップの問題やBorderに角丸を設定してPaddingを設定すると子要素の角も削られる問題もある。

TabbedPage

iOS

実体
Microsoft.Maui.Controls.Handlers.Compatibility.TabbedRenderer

カスタマイズする場合はTabbedPageとTebbedRendererのサブクラスを作成しそれをHandler登録する。
handlers.AddHandler(typeof(MyTabbedPage), typeof(MyTabbedPageRenderer));

Android

実体
src/Controls/src/Core/Platform/Android/TabbedPageManager.cs
src/Controls/src/Core/HandlerImpl/TabbedPage/TabbedPage.Android.cs

何故かAndroidはTabbedPageのpartial classとしてPlatform実装も書かれている。

カスタマイズ例

MyTabbedPage.cs
public partial class MyTabbedPage: TabbedPage
{
    public MyTabbedPage()
    {
#if ANDROID
        this.HandlerChanged +=MyTabbedPage_HandlerChanged;
        this.HandlerChanging += MyTabbedPage_HandlerChanging;
#endif
    }
}
MyTabbedPage.Android.cs
public partial class MyTabbedPage
{
    BottomNavigationView _bottomNavigationView;
    BottomNavigationView BottomNavigationView => _bottomNavigationView ??= GetBottomNavigationView();

    private void MyTabbedPage_HandlerChanged(object sender, EventArgs e)
    {       
        // ここでBottomNavigationViewをあれこれ頑張る
    }

    private void MyTabbedPage_HandlerChanging(object sender, HandlerChangingEventArgs e)
    {
        if (e.OldHandler == null)
        {
            return;
        } 
        // 後始末
    }

    BottomNavigationView GetBottomNavigationView()
    {
        // ReflectionでBottomNavigationViewを取得する
        var tabbedPageManagerInfo = typeof(TabbedPage).GetField("_tabbedPageManager", BindingFlags.Instance | BindingFlags.NonPublic);
        var manager = tabbedPageManagerInfo.GetValue(this);

        var bottomNaviInfo = manager.GetType().GetField("_bottomNavigationView", BindingFlags.Instance | BindingFlags.NonPublic);

        return bottomNaviInfo.GetValue(manager) as BottomNavigationView;
    }

この方法の場合はHandlerを上書きする必要は無い。

かむかむ

ビルド・発行

iOSのdotnet publishの注意事項

/p:CodesignKey="Apple Distribution: HOGE CO.,LTD. (XXXXXXX)"
みたいにCodesignKeyにカンマが含まれると実行できないのでカンマは%2cに置き換える必要あり
/p:CodesignKey="Apple Distribution: HOGE CO.%2cLTD. (XXXXXXX)"

かむかむ

不具合

Gridの子要素の高さが無視される

7.0.81/7.0.100 SDK 7.0.200 で確認。
→7.0.300 で解消済み

以下のような場合GridのPaddingの上下の値が無視される現象がある。

<Grid>
    <Grid Padding="30">
         <Label Text="Text" />
    </Grid>
</Grid>

ネストしていなくてもGridにPaddingが設定されてる場合に発生するかも?

対策は親のGridにRowDefinitions="Auto"を指定すること。

SafeAreaの設定がうまくいかない

SafeAreaをオフにした時にうまく反映されない。
SafeAreaを使わずに領域一杯を使いたい時は工夫が必要。

https://stackoverflow.com/questions/74945598/net-maui-unwanted-whitespace-at-bottom-of-screen-using-grid-layout

ScrollView

iOSで子のStackLayoutなどの子要素を動的に追加したり削除した時にサイズが変更されない。Xamairn.Formsの頃は問題なく更新されてた。
ScrollView+Layoutの代わりにCollectionViewを使って回避する。

https://github.com/dotnet/maui/issues/12727

Border / Frame

FrameはAndroidでサイズがバグっててまともにレイアウトできないので、Xamarinのままだと大きくレイアウトが崩れる。基本的にBorderを使うことで解決。

ただしBorderはBorderでiOSの方でコンテンツをClipできない問題がある。
画像を丸くクリップするような形で使う場合はiOSはFrame、AndroidはBorderみたいな分岐で対応するしかなさそう。

↓こういうWrapper作って暫定対応した。

public class RoundImage:ContentView
{
    public static BindableProperty SourceProperty = BindableProperty.Create(
            nameof(Source),
            typeof(ImageSource),
            typeof(RoundImage),
            default(ImageSource),
            defaultBindingMode: BindingMode.OneWay
        );

    public ImageSource Source{
        get { return (ImageSource)GetValue(SourceProperty); }
        set { SetValue(SourceProperty, value); }
    }

    Image _image;
    Frame _frame;
    Border _border;

    public RoundImage()
    {
        _image = new Image();
        _image.Aspect = Aspect.AspectFill;
        _image.SetBinding(WidthRequestProperty, new Binding("WidthRequest", source: this));
        _image.SetBinding(HeightRequestProperty, new Binding("HeightRequest", source: this));
        _image.SetBinding(Image.SourceProperty, new Binding("Source", source: this));


        // MAUIの不具合による暫定対応。
        // iOSのBorderでClipできない問題とAndroidでFrameでLayoutが崩れる問題
#if IOS
        _frame = new Frame();
        _frame.Padding = new Thickness();
        _frame.HasShadow = false;
        _frame.BorderColor = Colors.Transparent;
        _frame.IsClippedToBounds = true;
        _frame.SetBinding(WidthRequestProperty, new Binding("WidthRequest", source: this));
        _frame.SetBinding(HeightRequestProperty, new Binding("HeightRequest", source: this));
        _frame.Content = _image;
        Content = _frame;
#elif ANDROID
        _border = new Border();
        _border.Padding = new Thickness();
        _border.Stroke = Colors.Transparent;
        _border.StrokeThickness = 0;
        _border.SetBinding(Border.WidthRequestProperty, new Binding("WidthRequest", source: this));
        _border.SetBinding(Border.HeightRequestProperty, new Binding("HeightRequest", source: this));
        _border.Content = _image;
        Content = _border;
#endif
    }

    protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        base.OnPropertyChanged(propertyName);
        if(propertyName == WidthRequestProperty.PropertyName)
        {
#if IOS
            _frame.CornerRadius = (float)WidthRequest / 2f;
#elif ANDROID
            _border.StrokeShape = new RoundRectangle
            {
                CornerRadius = new CornerRadius(WidthRequest / 2d)
            };
#endif
        }
    }
}

Border その2

BorderとContentの間に謎のスペース。
Borderの線と内容をぴったりさせたくても出来ない。今のところ回避策なし。
Effectなどでレイアウトに直接線を設定するようにしたりすることで代替は可。

https://github.com/dotnet/maui/issues/7764

回避例 ボーダーを設定できるGrid

https://zenn.dev/link/comments/19c31ef509fc1c

MediaPicker

iOSでの向きが正しくない問題はXamarinを継承。
https://github.com/dotnet/maui/issues/11379

Xamarinの時と同様にXam.Plugin.Mediaの6.0.0(pre)以降を使うしかない。

ImageButton

AndroidでPaddingを設定するとサイズが正しく表示されない。
Padding0で使う分には問題ないがPaddingでタップ領域を多めに取っていた場合などは使えない。
回避策はImageButtonは使わずにContentViewでImageをWrapしてTapGestureを登録する方法に置き換える。
またはOnPlatformマークアップを使ってWidthRequestとHeightRequestにiOSは画像サイズ、Androidは画像サイズ+Paddingサイズを指定する。

https://github.com/dotnet/maui/issues/7927

かむかむ

Prism.Maui

Appクラスの罠

ドキュメントのどこにも書いていないがMauiでもAppクラスはPrismApplicationを継承するように変更しないと動かない。

public partial class App : PrismApplication
<prism:PrismApplication xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="http://prismlibrary.com"
             x:Class="Sample.App">
</prism:PrismApplication>

モーダル遷移の方法

最初のセグメントにUseModalNavigationをつけないと機能しない。完全に罠。
Nugetで公開されているバージョン(8.1.273-pre )では何やってもモーダル遷移ができないので注意

        await navigationService
            .CreateBuilder()                
            .AddSegment<NaviViewModel>(config =>
            {
                config
                .UseModalNavigation(true)
                .AddParameter(KnownNavigationParameters.Animated, animated);
            })
            .AddSegment<HogeViewModel>()
            .AddParameter(ParametersBase.ParameterKey, parameters)
            .NavigateAsync();

Builder使わない場合

navigationService.NavigateAsync("NaviPage?useModalNavigation=True/HogePage");
かむかむ

ボーダーを設定できるGrid

BorderGrid.cs
public partial class BorderGrid: Grid
{
    public static BindableProperty BorderColorProperty = BindableProperty.Create(
            nameof(BorderColor),
            typeof(Color),
            typeof(BorderGrid),
            default(Color),
            defaultBindingMode: BindingMode.OneWay
        );

    public Color BorderColor{
        get { return (Color)GetValue(BorderColorProperty); }
        set { SetValue(BorderColorProperty, value); }
    }

    public static BindableProperty BorderWidthProperty = BindableProperty.Create(
            nameof(BorderWidth),
            typeof(double),
            typeof(BorderGrid),
            default(double),
            defaultBindingMode: BindingMode.OneWay
        );

    public double BorderWidth{
        get { return (double)GetValue(BorderWidthProperty); }
        set { SetValue(BorderWidthProperty, value); }
    }

    public static BindableProperty BorderRadiusProperty = BindableProperty.Create(
            nameof(BorderRadius),
            typeof(double),
            typeof(BorderGrid),
            default(double),
            defaultBindingMode: BindingMode.OneWay
        );

    public double BorderRadius{
        get { return (double)GetValue(BorderRadiusProperty); }
        set { SetValue(BorderRadiusProperty, value); }
    }

    protected override void OnHandlerChanged()
    {
        base.OnHandlerChanged();
        UpdateBorder();
    }

    protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        base.OnPropertyChanged(propertyName);
        if(propertyName == BorderColorProperty.PropertyName ||
            propertyName == BorderWidthProperty.PropertyName ||
            propertyName == BorderRadiusProperty.PropertyName)
        {
            if(Handler != null)
            {
                UpdateBorder();
            }            
        }
    }
}
BorderGrid.iOS.cs
public partial class BorderGrid
{
    void UpdateBorder()
    {
        var handler = Handler as IPlatformViewHandler;
        handler.PlatformView.Layer.CornerRadius = (float)BorderRadius;
        handler.PlatformView.Layer.BorderWidth = (float)BorderWidth;
        handler.PlatformView.Layer.BorderColor = BorderColor.ToCGColor();
        handler.PlatformView.ClipsToBounds = true;
    }
}
BorderGrid.Android.cs
public partial class BorderGrid
{
    GradientDrawable _border;

    void UpdateBorder()
    {
        var handler = Handler as IPlatformViewHandler;
        var context = handler.PlatformView.Context;
        var view = handler.PlatformView;
        _border ??= new GradientDrawable();

        var width = (int)context.ToPixels(BorderWidth);
        var radius = (int)context.ToPixels(BorderRadius);

        _border.SetStroke(width, BorderColor.ToPlatform());
        _border.SetCornerRadius(radius);
        view.SetPadding(width, width, width, width);
        view.ClipToOutline = true;

        view.Background = _border;
    }
}