WPF経験者向けMAUI移行ガイド

※用語

  • PF → プラットフォーム
  • DI → Dependency Injection, データバインディングなど依存関係の登録に使う。
  • MVVM → Model, View, ViewModel にコードを分ける開発手法。ロジックとデザインを分離することができるが非常にわかりづらくなりがちなので一長一短
  • VSM → VisualStateManager, コードなしでウィンドウ幅やボタン押下などの状態別にコントロールの表示やデザインを切り替えられる。
  • VS → Visual Studio (特にWindows版)

目次

結論 

MAUIのデメリット

  • 苦痛
  • Webアプリのほうが遥かに楽&デザインも自由&ノウハウが豊富&早い
  • 仕様がコロコロ変わる
  • WPF、UWP とはほぼ別物。部分部分で妙に過去のUWPのノウハウなどを要求されるので全くのモダンアプリ未経験だとキツイ
  • WPF、UWP、Xamarin と互換性がない。変に似たメソッド名とかあるわりに破壊的変更が加わっているので検索汚染が凄まじい
  • Border の CornerRadius がなくなった。(なんでだよ!?)
  • Community Toolkit にあまりにも頼りすぎている。必要なものが標準になさすぎ
  • いっそネイティブを学んだほうが早い
  • プラットフォームごとに結局変なところで動いたり動かなかったりするのがめんどくさい。逆に最初から違うものとして取り組むほうが労力はかかるがストレスはない
  • MAUIであることの必要性が薄い
    • Windows (WPF, WinForm) はWin32やC++アンマネージドもいける明確なネイティブアプリであることのメリットや必然性があるが、モバイルにそれはない。
  • カスタム性が低い
  • ネイティブだが時にWebアプリより重い (特にCanvas相当のGraphicsView。グラフィックス担当が一番重くてどうすんねん感)
  • MAUIに限らないがストアアプリというシステム自体が審査や登録料、準備しなければならないリソースや権限の管理などめんどくさすぎる
  • 一番ネイティブで Python が動きそうな Android くんで Python を使う方法が用意されていない。
  • 数々の .NET / .NET Standard 対応とか謳っている Nuget ライブラリの多くはマルチPF非対応。何のための脱 Framework だったのか……

MAUIのメリット

※少ない

  • Animationが簡単。WPFのそれが難しすぎたとも言う
  • Spacing で中身のマージン調整が簡単になった (けど結局最初と最後だけ調整するということはできないので何やかんや空の Border で調整しがち)

概要:

MAUIの種類など

  • MAUI
    • 今回の記事対象はこちら
    • 基本的なC#.NETでのマルチPF開発。
    • win, android, apple に対応。
    • apple系は実機がないとリリースはおろかデバッグもできないので注意。
  • MAUI Blazor
    • Blazor というHTMLとCSSとJSとC#のいいところどりしたようなプラットフォームでの開発。アプリとブラウザ両方での動作を視野にいれるならこれ。
    • HTML/CSSのデザインノウハウが使えるほか、jsもいける。
    • 書き方が少し独特で慣れは必要。ASP.NETの経験があると尚良い。
    • サーバーで動くタイプと、Clientで動くタイプの2種類がある。
  • MAUI Blazor Hybrid
    • Blazor にネイティブホストとしてXAMLやC#でのネイティブAPI呼び出しに対応したタイプ。
    • ネイティブとBlazorWebViewを組み合わせることが可能で、相互データバインディングも可能。(ちょっとめんどくさいけどやり方は後述)
    • 注意点として、ネイティブということはブラウザで起動した際にネイティブ部分は動作しないことになる。そのためアプリとして起動されたときとブラウザとして起動されたときを判定しなければならない。

よく使うXAMLタグなどの対応表

WPF XAMLMAUI XAML備考
TextBlockLabel 
TextBoxEntry 
WidthWidthRequestMinMaxも同じ
HeightHeightRequestMinMaxも同じ
ListViewListView / CollectionViewCollectionViewのほうが多機能、Reorder(並べ替え)対応
Image > StretchImage > Aspect 
CornerRadius後述 
UserControlContentView 
BitmapImagePlatformImage 
鋭意編集中…

※10年ぶりにやる人向けのこれまでのC#.NET関係の履歴

  • C#言語のバージョン
    → 記法や仕様変更など。ラムダ式やnew型推論など知っていると楽だがどんどん難解になり可読性が下がっていくのが玉に瑕。
  • .NET バージョン
    → .Net Framework → .NET Core/Standard → .NET と何度も名前が変わっている。Ubuntuのように長期サポートバージョンとそうでないものがあり、採用時はできるだけ長期安定版を選んだほうがいい。
  • UI の作り方
    • WinForms
      → 昔ながらのレイアウト方式。UnrealEngine4や5も似たような形式
    • WPF
      → モダンでGPU支援があり、Relative なレイアウトにも対応。XAMLというHTMLライクなUIデザインが可能。XAMLはここから採用
    • UWP
      → Win10以降の半透明:Acrylicなウィンドウスタイルや通知、センサーやカメラなど最新のOS機能に対応。だがファイルアクセスがガッチガチに制限されWin32時代の資産も使えない、そしてバグが多く結構落ちやすいなどで不況。ただしマニフェストやasync/await、VisualStateManager、AppShellやページシステムとNavigate、モダンUI特有の動作などの面でUWPを経験していないと、MAUIでのモダン開発には苦労するかも?
    • WinUI
      → UWPからUIだけ取り出したもので、WPF等にも組み合わせられる。WinUI 3 は Windows 11 相当。UWPから引き継いでしまったのか、若干バグがあるので注意。(たまによく落ちる)

MAUI 逆引き豆知識 (ShortTuts)

システムと基礎¦Windowsでのデバッグについて

デフォルトではMsixPackageモードがONになっているが、デバッグできない場合はプロジェクトプロパティからオフにする。
Pasted image 20250706143507.png

ただしパッケージ作成時(公開時)には戻さないといけないので、自動化するなら .csproj ファイルを手動編集して、WindowsPackage~のタグを↓のようにする。

<WindowsPackageType Condition="'$(Configuration)'=='Debug'">None</WindowsPackageType>
<WindowsPackageType Condition="'$(Configuration)'=='Release'">MSIX</WindowsPackageType>

システムと基礎¦フォントを追加する

デフォルトではOpenSansが割り当てられているが、欧文フォントなので日本語だとMeiryoUIなど置き換わってしまう。そのため、できれば別のフォントに置き換えたい。

おすすめ:

IBM PlexSans JP
Pasted image 20250706135948.png
https://fonts.google.com/specimen/IBM+Plex+Sans+JP
ヒラギノに近い表現が可能で、Windowsでも美しい。
Regularでは少し見づらいので、Mediumくらいがちょうどいい。
古いバージョンではotfだったが、ベースラインがずれるのでその場合はttfを再ダウンロードしよう。

NotoSansJP
Pasted image 20250706140023.png
https://fonts.google.com/noto/specimen/Noto+Sans+JP
クセがなく使いやすい。

BIZ UDゴシック
Pasted image 20250706135915.png
https://fonts.google.com/specimen/BIZ+UDGothic
UDというのはユニバーサルデザインのことで、UIに使用しても見やすいという特徴がある。(他のフォントが別にUDではないというわけでもない)

フォントの追加方法:

📂 Resource
📂 Fonts

にダウンロードしたttfを配置する。
その後「MauiProgram.cs」にフォントを追加している行があると思うので、そこに追加する。

fonts.AddFont("IBMPlexSansJP-Medium.ttf", "IBMPlexSansJPMedium");

フォントの利用方法:

FontFamilyを指定する。このときの名称は↑で指定した名前になるので注意。

FontFamily="IBMPlexSansJPMedium"

システムと基礎¦csファイルの分割 (おまけ)

これはMAUI特有ではないのだが、クラスファイル化は嫌だがファイルが長くなるのも嫌なら、MainPageを分割することができる。(WPFのMainWindowもOK)

例:
📂 プロジェクト親
📂 Pages
📂 MainPagePartial
MainPage_~~~.cs
↑↑↑
・App.xaml
・App.xaml.cs
・MainPage.xaml
・MainPage.xaml.cs

MainPage_~~~.cs

namespace ~~;

public partial class MainPage : ContentPage
{
	  ...
}

※~~~は担当する機能など好きにする

MainPage のような partial class はファイルに分割されても機能するので、
別に他で使うあてもないのにわざわざファイル分割目的だけでクラスファイルに分けているのならこのような感じでOK. これでいちいちインスタンスを作成してデータの受け渡しのためにインターフェイスを実装して……という手間も省ける。
(namespace も Visual Studio はフォルダごとに細分化を提案してくるが、よほど巨大化してなければプロジェクト名だけのほうが楽。)

※そもそもMVVM開発が持ち上がる理由のひとつに、従来の手法ではコードが長くなりすぎてスクロール量が増え、可読性が下がるというデメリットがある。
また、Githubなどでチーム作業をしているといくら差分が見れるとは言っても基本的にはファイル単位での管理になるので、人数を増やす=ファイルを分割する になるため、どうしてもMVVMのように人=ファイル=機能とせざるをえないという事情がある。

とはいえデータバインディングは素のコードビハインドよりもパフォーマンスが落ち、バインディングエラーがあると特に顕著である。
また、メンテナンスするとわかるのだが、MVVM開発の最大のデメリットは「わからなくなる」ことである。年月が経つと自分が書いたコードでさえ何がどこにあるのかわからなくなりがちで、MVVMではより複雑化する傾向にある。
また、MVVMの「お作法」を意識しすぎるあまり、本来はできるのにテンプレやお作法から外れたことができなくなることもある
そのため機能で分割してしまうMVVMよりも、「エリア」で機能もデザインもいっしょに分割できる partial class 的なアプローチのほうがメンテナンス性は良い。……はず。

デザイン¦ドロップシャドウ

Effectという名前はなくなったものの、基本的な使い方は一緒。
Borderの例だとこんな感じ。

<Border.Shadow>
	<Shadow
		Brush="{StaticResource Shadow000}"
		Radius="36"
		Opacity="0.15"
		Offset="0, 12"
		/>
</Border.Shadow>

デザイン¦角丸(CornerRadius)

指定方法が変わった。
Borderの例だとこんな感じ。

<Border.StrokeShape>
	<RoundRectangle CornerRadius="16" />
</Border.StrokeShape>

デザイン¦スライダーのつまみをなくす

SliderのThumbに透明な画像(4×4.pngなど)を指定するのが最も手っ取り早い。

<Slider
	ThumbImageSource="transparent_4x4a.png"
	/>

デザイン¦ボタンに画像を設定する

デフォルトではボタンの中に画像は設定できないので、ImageButtonを使うかBorderの中に入れる。Borderの場合、タッチイベントはジェスチャで設定する。
角丸もシャドウも入れるとこんな感じになる。

<Border
	Stroke="Transparent"
	StrokeThickness="0"
	>	
	<Image
		Source="パスは英字で始まって終わること.png"
		Aspect="AspectFill"
		/>
	<Border.GestureRecognizers>
		<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
	</Border.GestureRecognizers>
	<Border.StrokeShape>
		<RoundRectangle CornerRadius="16" />
	</Border.StrokeShape>
	<Border.Shadow>
		<Shadow
			Brush="Black"
			Radius="36"
			Opacity="0.15"
			Offset="0, 12"
			/>
	</Border.Shadow>
</Border>

デザイン¦カスタムする (UserControl)

ファイルから ContentView.xaml を追加し、カスタムする。

デザイン¦アイコンボタンやToggleButton

【2025-07-09 ベータ版】

アイコン+テキストが表示できてToggleButtonとしても機能するものを作ったので↓をDLしてお使いください。名前空間はご自身のものに置き換えてください。

https://www.mediafire.com/file/s4pjylhfzo5zo7x/ButtonIconText.xaml.zip/file

また、フォントファミリーとしてFontAwesomeを↓で登録しておいてください。ファイル名は適宜置換。

fonts.AddFont("fa-solid-900.otf", "FA");

IsToggleEnabled を切り替えると普通のボタンとしても使えます。

デザイン¦リストやコレクション

ドラッグアンドドロップ並べ替え

CollectionViewを使い、CanReorderItems = “True” を指定するだけでDnDでの並べ替えが可能。

<CollectionView
	CanReorderItems="True"
	>	

コンテキストメニュー(右クリックメニュー)

SwipeItemsとして実装。
https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/controls/collectionview/populate-data?view=net-maui-9.0#context-menus

または、Pageの場合は

	DisplayActionSheet("タイトル", "キャンセル", null, "項目1", "項目2" ...);

グループ化する

IsGrouped = true し、Group系のTemplateを実装する。
https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/controls/collectionview/grouping?view=net-maui-9.0

引っ張って更新 を実装する

RefreshViewの中に配置し、IsRefreshing とCommandを実装する。
XAMLとC#の例↓ どちらかだけでOK
https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/controls/collectionview/populate-data?view=net-maui-9.0#pull-to-refresh

特定の項目までスクロールする

ScrollTo() で可能。アイテム指定、Index指定、グループ内の何番目、など可能
https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/controls/collectionview/scrolling?view=net-maui-9.0

デフォルトでは最小限の表示だが、中央までスクロールするなら↓

collectionView.ScrollTo(<項目の名前など>, position: ScrollToPosition.Center);

さらにスワイプスクロール時にスナップさせることも可能。

SnapPointsType

選択したアイテムを取得する

タッチで SelectedItem が変わる都合や、データバインディング時にたとえばアイテム内のボタンを押したときは SelectedItem からのインデックス取得を使えないときがある。アイテム内のボタンを押した時、元のアイテム自体を取得したいときはこうする。

private void CollectionView1_ItemTemplate_MoveDown(object sender, EventArgs e)
{
	var element = sender as VisualElement;
	DataItem dataItem = null;
	
	// 親をたどって BindingContext を探す
	while (element != null && dataItem == null)
	{
		dataItem = element.BindingContext as DataItem;
		element = element.Parent as VisualElement;
	}
	
	if (dataItem == null)
	{
		Debug.WriteLine("BindingContextが見つかりませんでした");
		return;
	}
	
	var items = collectionView1.ItemsSource as ObservableCollection<DataItem>;
	int index = items.IndexOf(dataItem);

}

この例ではXAMLでは CollectionView の ItemTemplate 内にボタンが置いてあって、そのクリックイベントがこれ。
そしてバインディングしているのはなんらかの ViewModel (ObservableCollection) の中にある DataItem がアイテムの本体(DataModel)だとする。

【BUG?】VisualStateManagerの挙動について

ウィンドウ幅に応じて自動で非表示にするなど、VisualStateManager は有用。

ちなみに WPFやUWPとも仕様が違うようで、 VSM.VSGroups > VisualStateGroupList > VisualStateGroup > VisualState という階層なので注意。(ListとGroup2つもいるのかこれ……??)

<ContentPage.Resources>
	<Style TargetType="Grid">
		<Setter Property="VisualStateManager.VisualStateGroups">
			<VisualStateGroupList>
				<VisualStateGroup x:Name="CommonStates">
					<VisualState x:Name="Normal">
						<VisualState.Setters />
					</VisualState>
					<VisualState x:Name="Selected">
						<VisualState.Setters>
							// ここに書く
						</VisualState.Setters>
					</VisualState>
				</VisualStateGroup>
			</VisualStateGroupList>
		</Setter>
	</Style>
</ContentPage.Resources>

ただしカスタムコントロールでうまく Binding できていなかったりすると動かないときもある。

他にもOpacity&InputTransparent(イベントを発火させず通過させる)のもNGらしい(手元の.NET8ではうまく動かず)。
そのためやり方としては VSM ではなく、 ViewModel 経由で IsSelected を持っておきバインディングする。

モデル(Item)のcs

private bool _isSelected;
public bool IsSelected
{
	get => _isSelected;
	set
	{
		if (_isSelected != value)
		{
			_isSelected = value;
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
		}
	}
}

public event PropertyChangedEventHandler PropertyChanged;

xaml

<CollectionView.ItemTemplate>
	<DataTemplate>
		<Grid
			...
			>	
			<Grid IsVisible="{Binding IsSelected}">
				<Button ... />
			</Grid>

cs

private void CollectionView1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    foreach (DataItem item in e.PreviousSelection)
        item.IsSelected = false;

    foreach (DataItem item in e.CurrentSelection)
        item.IsSelected = true;
}

ほかにも Scale で0→1などもうまく動いていないので、おそらくバグだと思われる。

デザイン(ロジック)¦重い読み込み処理とUIへの反映

厳密にはデザインとは少し違うが、CollectionView と切っても切り離せない大量のデータ読み込みと、それのUIへの反映について。

結論から言うと、

  • 重い処理
    • await Task.Run ~ の中で処理。ReadTextAsync とか。
  • UIへの反映
    • MainThread で処理すべき。awaitなどTask系ではなく、MainThread.Begin~ () の中で実行する。

await Task ~ の中でUI処理までやってしまうと遅くなるのでひとつにまとめるのではなく、例えまとめる場合でもUI処理は MainThread.Begin~ の中で処理するのがおすすめ。

デザイン¦いい感じに並べる

Gridのプロパティ: RowDefinitions や Spacing系のプロパティを使う。Spacing はStackLayout などでも使える。

<Grid
	RowDefinitions="120, *, *, *, auto, ... "
	RowSpacing="8"
	>
	...
</Grid>

コード¦データバインディング用プロパティの作成

基本はWPFと変わらない。使うのはBindableProperty。

public int IconSize
{
	get { return (int)GetValue(IconSizeProperty); }
	set { SetValue(IconSizeProperty, value); }
}

public static readonly BindableProperty IconSizeProperty
	=
	BindableProperty.Create
	(
		nameof(IconSize), typeof(int), typeof(ButtonIconText), 16
	);

コツ:

  • プロパティに get / set をつけてバインディング用プロパティに割り当て
  • BindablePropertyのほうでは、以下を設定する。
    • プロパティ名
    • 親クラス
    • プロパティの初期値

あとは普通にバインディングする。

<Label FontSize="{Binding IconSize}">

カスタムコントロールなど、バインディングが複数階層で重複するようなときは、明確に名前をつけて参照する。

FontSize="{Binding プロパティ名, Source={x:Reference controlRoot}}"

コード¦MessageBoxやダイアログ

Page以外からでも呼び出せるやつ:

await Application.Current.MainPage?.DisplayAlert("test", "test", "OK");

コンテキストメニューや選択肢のあるダイアログとしては、

var res = await DisplayActionSheet("タイトル", "キャンセル", null, "選択肢1","2");

ファイル名の入力を求めるなら prompt を使う。

var newname = await DisplayPromptAsync( 省略 )

コード¦FileIO¦ファイルの保存やOpen、Folderのダイアログ

Picker系のメソッドを使えばOK。
ただしSaveだけ実装されておらず、楽をしたいなら CommunityToolkit のものを使用する。Apple系は仮想ファイル扱いになるようで、注意が必要。

Open:

FolderPicker, FilePicker など。

FileSaver:

// CommunityToolkit.Maui をnugetで追加

// builder.UseMauiApp<App>().UseMauiCommunityToolkit();
// ↑を MauiProgram に追加

using CommunityToolkit.Maui.Storage;
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(stringText));

var result = await FileSaver.Default.SaveAsync(fileName, stream);

// result からパスなどを拾える

コード¦FileIO¦相対パスと絶対パス

絶対→相対パス

var RelFromAbs = Path.GetRelativePath(Directory.GetParent(sourcepath).FullName, absPath);

相対→絶対パス

var sourceDir = System.IO.Path.GetDirectoryName(SourcePath);
var absPath = Path.GetFullPath(relPath, sourceDir);
// source指定しないとアセンブリ実行パスからの解決になるので注意

※パスの相互変換はWPFと変わらないものの、Windows 以外の仮想ファイルパスにはお気を付けを。

コード¦FileIO/Network¦SMBアクセス

参照:
https://try-dot-net-core.hatenablog.com/entry/2016/12/28/140429

コード¦FileIO¦Androidでの全ファイルアクセス

Android では FilePicker 系で選択されたファイルはすべてキャッシュ上の仮想パスになるので注意。

全ファイルへのアクセス権要求は、外部ストレージ(Manage External Storage)の権限を有効化した上で、

protected override void OnAppearing()
{
	base.OnAppearing();
	string package = AppInfo.Current.PackageName;
	var uri = Android.Net.Uri.Parse($"package:{package}");
	Platform.CurrentActivity.StartActivityForResult(new Android.Content.Intent(Android.Provider.Settings.ActionManageAppAllFilesAccessPermission, uri), 10);
}	

とする。

コード¦処理待ちのローディングアイコン

ファイルリストなど、長い処理を待っている間はローディングアイコンでも表示してあげるとユーザーフレンドリー。(Toolkit)

LoadingPopup.xaml

<?xml version="1.0" encoding="utf-8" ?>
<toolkit:Popup
    x:Class="YourAppNamespace.LoadingPopup"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    IsLightDismissEnabled="False"
    Size="200,200"
    BackgroundColor="#80000000"
    >
    <Frame BackgroundColor="White" CornerRadius="12" Padding="24" HorizontalOptions="Center" VerticalOptions="Center">
        <StackLayout>
            <ActivityIndicator IsRunning="True" />
            <Label Text="読み込み中..." HorizontalOptions="Center" />
        </StackLayout>
    </Frame>
</toolkit:Popup>

LoadingPopup.xaml.cs

public partial class LoadingPopup : CommunityToolkit.Maui.Views.Popup
{
    public LoadingPopup()
    {
        InitializeComponent();
    }
}

使う際は重くなりそうな手前で表示しておく。
また、処理の合間にわずかに Task.Delay をかましておくと良い。
ObservableCollection でもこの Delay がないとViewでアイテムが追加されていく様子がなく、全部終わってからはじめて更新されてしまうので体感が長く感じる。
一方で Delay を都度入れるということは当然遅くなるので、Count % 10 == 0 など特定の場面でだけ Delay を入れるようにしたほうが良い。

var popup = new LoadingPopup();
this.ShowPopup(popup);

// 重めの処理ループ
	await Task.Delay(10); // UIスレッドを解放

 popup.Close();

コード¦BitmapImage

PlatformImage.FromStream(stream) を使う。

var buffer = new byte[] ~~~; // 例としてバイト配列を用意
using var stream = new MemoryStream(buffer);
var pimg = PlatformImage.FromStream(stream);

PlatformImageの場合、リサイズやCanvasへのDraw、Paint扱いなどもできるらしい。

コード¦TopMost(最前面表示)

MauiProgram に下記を追記する。using を追加し、builder系の流れにWindows限定の処理を挿入する。

using Microsoft.Maui.LifecycleEvents;
#if WINDOWS
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Windows.Graphics;
#endif

~~~

#if WINDOWS
 builder.ConfigureLifecycleEvents(events =>
 {
         events.AddWindows(windowsLifecycleBuilder =>
         {
                windowsLifecycleBuilder.OnWindowCreated(window =>
                {
                      var handle = WinRT.Interop.WindowNative.GetWindowHandle(window);
                      var id = Win32Interop.GetWindowIdFromWindow(handle);
                      var appWindow = AppWindow.GetFromWindowId(id);
                      var presenter = appWindow.Presenter as OverlappedPresenter;                                
                      appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
                      presenter.IsAlwaysOnTop = true;
                  });

             });
});
#endif

コード¦Media¦メディアを再生する

UWP で存在していた MediaElement が例によって CommunityToolkit にて実装されている。少し仕様は違うが動画や音楽などさまざまなメディアを扱えるのでUWPに慣れている人は特にこれが一番楽だと思われる。
https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/maui/views/mediaelement?tabs=windows

Github Copilot によるコード補完をする場合、バージョンによって構文が変わることもあるので気をつけること。

また、UWP と違ってシークバーの位置などプロパティの多くが readonly になったこともあり、C#.NET の利点であるデータバインディングによる簡潔なUIビルドが難しくなっている。
そのため、従来型の Changed 系イベントにいちいちフックしてやること。
/pub

CommunityToolkit MediaElementのバグ

MediaElement にはいくつかバグがあり、MediaEnded にて MediaSource を指定すると落ちる。そのため必ず MainThread で MediaSource をセットすること

private void MediaElement1_MediaEnded(object sender, EventArgs e)
{
	MainThread.BeginInvokeOnMainThread(() =>
	{
		mediaElement1.Source = MediaSource.FromFile(FilePath);
	});
	 
}

※追記: MediaEnded は挙動が安定しないので、代わりに PositionChanged で自前で載せたほうが吉。

private void mediaElement1_PositionChanged(object sender, CommunityToolkit.Maui.Core.Primitives.MediaPositionChangedEventArgs e)
{
	if (mediaElement1.Position >= mediaElement1.Duration - TimeSpan.FromMilliseconds(500))
	{
		Debug.WriteLine("MediaElement1 End Position");
		if ( mediaElement1.CurrentState == MediaElementState.Stopped
			|| mediaElement1.CurrentState == MediaElementState.Playing )
		{
			mediaElement1_MediaEndedAlt(sender, e);
		}
	}
}

※MediaEnded 以外でも、await(非同期) 処理にて COMException で落ちるときは MainThread で実行する。

MainThread.BeginInvokeOnMainThread
(
	async ()
	=>
	{
		await ...
	}
);

コード¦デバイスの挿抜検出

オーディオデバイスの変更(ヘッドフォンが抜かれたら再生停止など)は、↓のファイルをDLする。

https://www.mediafire.com/file/pvf36hjeah3vry7/AudioDeviceMonitoring.cs/file

※名前空間だけ気を付けて欲しい

使用時は上記ファイルの名前空間を入れたうえで↓のようにする。

AudioDeviceService serv = new AudioDeviceService();

AudioDeviceService.OnHeadphoneStateChanged += inserted =>
{
	Debug.WriteLine($"Headphone state changed: {(inserted ? "Inserted" : "Removed")}");
	// mediaElement1.IsMuted = !inserted; // 抜かれたらミュート
};

serv.StartMonitoring();

※どこかでStopMonitoringを呼ぶこと。

参考: Windows: UWPでも使った DeviceWatcher を使う。

参考: iOS:

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Notifications/Articles/NotificationCenters.html#//apple_ref/doc/uid/20000216-BAJGDAFC

コード¦FileIO¦アプリケーション間のドラッグ・アンド・ドロップ

プラットフォームによって違う。Windowsなら XAML の何かの要素に GestureRecognizer + Drop系 を割り当てて、その Drop イベントにてプラットフォームごとに書く。Windows なら StorageItem をUWP同様MAUIでも使用する。

#if WINDOWS
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
#endif
async void OnDropGestureRecognizerDrop(object? sender, DropEventArgs e)
{
    var filePaths = new List<string>();

#if WINDOWS
    if (e.PlatformArgs is not null && e.PlatformArgs.DragEventArgs.DataView.Contains(StandardDataFormats.StorageItems))
    {
        var items = await e.PlatformArgs.DragEventArgs.DataView.GetStorageItemsAsync();
        if (items.Any())
        {
            foreach (var item in items)
            {
                if (item is StorageFile file)
                    filePaths.Add(item.Path);
            }
        }
    }
#endif

    string filePath = filePaths.FirstOrDefault();

    // Process the dropped file
}

ドラッグ位置の取得はドラッグ系イベントなら e.GetPosition(null); で取得できる。

【BUG】CollectionViewに設定すると、バグなのかAndroidではスクロールが動作しなくなるので注意。

その他¦デバッグ時のエラーなど

●Windows で Msix Package がどうのこうのでデバッグできない
→プロジェクトプロパティで、Windows のデバッグ時だけ Msix-Package の生成をオフにする。

●Android にてフォントやアイコンなどが反映されない
→たまにある。ビルド → クリーン して リビルド する、クリーン後にエクスプローラーで bin, obj を削除してリビルドするなどを試してみる。原因は不明なので下手にリソースファイルをいじらないこと。

●Windows で、普通のデバッグ時はよかったのにパッケージ作ってインストールしてからだと起動後すぐに落ちる
→OnAppearing をオーバーライドして書いているとうまくいかないことも。 Loaded に変更するなどする(もしかしたらMAUIのバグかも?)

●なぜか急にビルドが通らなくなった。
→以下のコマンドでソリューションをリセットしてみる。(Intel CPUの不具合??)

dotnet restore <slnファイルの絶対パス>

その他¦エミュレーター(Android)へのファイル受け渡し

ツール > Android から adb コマンドを使う。(Android Studioのエミュレーターはドラッグ・アンド・ドロップもいけるらしいが、Visual Studio からの起動は非対応??)

adb push "ファイルパス" /sdcard/download/

※この「sdcard」はPixel系デバイスのエミュレーターでは sdcard ではなく内蔵ストレージのこと。

その他¦Android でのパフォーマンス分析

バッテリー消費量などを詳細に分析するなら Perfectto UI を使用する。adb と Web USB (Chrome系) が使えるならOK。

https://ui.perfetto.dev/#!/record/target

その他¦MacでのiOSシミュレーターリモートデバッグ

2025-07-19: Macのバージョンによりデバッグできないようなので注意。

まず、以下をMacにて準備する。

  1. リモートログインを有効化し、フルディスクアクセスを許可。たまに閉じると戻る (なぜ……??)。
  2. Xcode をインストール (App Store からは入手できないことが多いので、Appleのディベロッパーサイトがおすすめ)
    • More – Downloads – Apple Developer
    • Command Line Tools を別途入れないほうがいい。競合する。万が一入れてしまった場合、↓のコマンドで切り替えられる。
    • sudo xcode-select -s /Applications/Xcode.app
  3. Mono をインストール
  4. dotnet SDK をインストール (PJと同じバージョン、Arm64)
  5. Rosetta をインストール

※詳細:
https://learn.microsoft.com/ja-jp/dotnet/maui/ios/pair-to-mac?view=net-maui-9.0

その後、VSのメニューから ツール > iOS > Pair to Mac を選択。

ウィザードが表示されるのでOKを押して、おそらく自動で出てきているはず(出ていなければ手動入力)なので選択して接続する。

接続中、「Xamarin.iOSないけどインストールするか?」と聞かれたらYesを選択。
(これが出ないまま進んでいるとうまくいかないので、↓のフォルダを消してやり直す。)

Windows:
%localappdata%\Temp\Xamarin\

Mac:
"$HOME/Library/Caches/Xamarin" の XMA フォルダ

※Macでのデバッグでよくあるエラー:

😥 出力ウィンドウに「オブジェクト参照が~」とか言われる
→ Macに dotnet SDK を入れる

😥 一通り終わった後接続完了せずに特にエラーも吐かずに終わる。でもデバッグボタンの Simulator の一覧には出ない
→ ペアリング失敗。Macに Command Line Tools を別途入れているとそうなる。切り替えるなら
xcode-select -p
で現在の開発環境PATHを確認でき、
sudo xcode-select -s /Applications/Xcode.app
で切替。

😥 シミュレーターってどこ?
→ ターミナルで open -a Simulator し、メニューの File > Open 以下にある。

😥 XcodeバージョンとLinker behaviorなるものを変えろとか言われてる
→ できれば素直にXcodeのバージョンを上げて対応させたい。.NET 8 + macOS 14系 + Xcode 16 + iOS 18 が盤石?あまり macOS のバージョンを下手に上げると動かなくなるという報告もあり避けたい。非推奨だが、上げたくないときは以下の画像のようにまずはプロジェクトプロパティの Linker を Link Framework SDKs Only または All にしてみる ( All にすると未使用っぽいコードを実行時に自動で消すので注意 )。

うまくいくと iOS シミュレーターへのリモートデバッグが Windows から可能になる。

😥 FilePicker でファイルが選べない
→ 残念ながらシミュレーター内のアプリはあくまで「実機っぽく動くそれっぽいアプリ」であり、「ファイル」アプリなどは開く、保存などの動作ができない。実機が必要。また、iCloud などを使うならシミュレーターでは macOS Sonoma 以上が、そうでなくともプロビジョニングの設定に Apple Developer Account (有料) が必要になるので、本気で apple 系にリリースするならそこそこのお金と準備が必要になる。

 

その他¦switch 文の楽な書き方

まず予め enum を作る。(略)

switch 文を入力する際、Intellisense の補完時に「Tabを2回押す」。ここでは Enter で確定しないこと。
Pasted image 20250708211205.png

次に中の変数がハイライトされている状態で入力し、ここで「Enterを2回押す」こと。
Pasted image 20250708211312.png

すると自動で enum で作ったものが勝手に作られるので便利。
Pasted image 20250708211409.png

アプリを公開する

アプリ編の記事があるのでそちらも参照。
https://bibohlog.ltt.jp/?p=1099

以下の記事では↑のアプリ関連のジェネラルな情報以外の、MAUI特有の情報を扱う。

アプリパッケージの作成

  • ツールバーのプルダウンから Release に変更する
    • デバッグ時にMsixPackageの設定を変えた場合、Release かつ Publish 時には Msix に戻しておく。
      https://learn.microsoft.com/ja-jp/dotnet/maui/windows/deployment/publish-cli?view=net-maui-9.0
  • デバッグボタン右隣のターゲット設定 (Windows, Android Emulator … ) に応じて発行対象プラットフォームが変わるらしい。つまり一気に全プラットフォーム分Publishしたいならバッチビルドを組む必要があるかも
  • ソリューションエクスプローラーからプロジェクトを右クリックして「発行」(Publish)。

⚠ 注意
基本的にプロジェクト設定をGUIエディターで編集するのは推奨しない。
.csproj をXMLテキストエディターで編集するのがおすすめ。以下、その前提。

パッケージのビルド、署名について:

Windowsにおいてのパッケージ作成は プロジェクトを右クリック > 発行 (Publish) から。その後の選択や署名については、

  • サイドローディングで完結、サイトや社内でのみ配布(ストア配布なし)……
    • 自己署名する。(しないとインストールできない。まだない場合は作成ボタンで適当に作成)
    • Msix 作成時に一緒に証明書も出力されるのが特徴。
    • 自分やユーザーには証明書をインストールしてもらう。インストール時に場所指定で「信頼されたルート」を選んでもらうこと
    • 証明書インストール後は Msix ダブルクリックでインストール可能。
  • ストアで配布する……
    • 自己署名しない。ストア登録時にMS側で自動署名してくれる。(無料)
    • Platforms > Windows > Package.appxmanifest をテキストエディターやXMLエディターで直接編集し、Publisherの CN= 以下を、Partner Center 上での Identifier 情報に置き換える
    • Partner Center へのリンク (開発ポータル的な) :
      https://partner.microsoft.com/dashboard/home
    • 証明書が出力されず、ダブルクリックでのインストールはできない。
    • Partner Center へのアップロードは appxbundle でなくても、msix でOK。
左: MSの Partner Center > App/Game > App Overview > Product Identity 画面。
右: Package.appxmanifest のXMLエディター

なお、お金があるなら Verisign などコード署名を正式に依頼して .cer ファイルをもらうという手もある。これだとサイドローディングでもストアでも通用する(はず)。

参考:
https://learn.microsoft.com/ja-jp/dotnet/maui/android/deployment/publish-ad-hoc?view=net-maui-9.0

リンク先にも書いてあるように、パスワードを忘れたり署名ファイルを紛失しないように。

急にパッケージの作成(Publish)に失敗するようになった場合

ソリューションのクリーン→リビルド→一旦閉じてエクスプローラーから bin / obj を両方削除 → ソリューションを開き直す → デバッグ実行(ビルドのみではない) → プロジェクトを右クリックして「発行」(Publish)……で解決することも。本当によくある。

また、AndroidビルドにおいてプロジェクトプロパティのGUIエディターで最後の位置にあるSignまわりを下手にいじると今後ビルドが通らなくなることも。万が一いじった場合はXMLエディターで直接プロジェクトプロパティを編集し、該当箇所を戻すかクリーンな新規プロジェクトから戻したほうがいい。一応、クリーンな状態は↓の通り。

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android|AnyCPU'">
  <AndroidPackageFormat>apk</AndroidPackageFormat>
</PropertyGroup>

バージョン番号の自動Increment

Release ビルド かつ パッケージング時のみに日付時刻ベースのバージョン番号を自動付与するなら .csproj のバージョン系の記述を↓のように書き換える。(注意: ApplicationVersionがint32の最大値を超えないこと。2025年だと例えば 202512301259 のようになり、超えるので要注意。Android ではバージョン番号のロールバックはできない。)

※PropertyGroupの中にデフォルトではバージョン番号が入っているが、そこをコメントアウトしてその外側に↓のPropertyGroupを作ること

<!– Debug 共通設定 –>
<PropertyGroup Condition=”‘$(Configuration)’ != ‘Release'”>
<ApplicationDisplayVersion>2025.07.1000</ApplicationDisplayVersion>
<ApplicationVersion>1000</ApplicationVersion>
</PropertyGroup>

<!– Release時のみ自動バージョン生成 –>
<PropertyGroup Condition=”‘$(Configuration)’ == ‘Release'”> <!– 現在時刻と起点時刻の ticks –> <_now>$([System.DateTime]::UtcNow.Ticks)</_now> <_base>$([System.DateTime]::Parse(‘2025-01-01T00:00:00Z’).Ticks)</_base> <!– ticks差分:1分 = 600000000 ticks –> <_diffTicks>$([MSBuild]::Subtract($(_now), $(_base)))</_diffTicks> <_minutes>$([MSBuild]::Divide($(_diffTicks), 600000000))</_minutes> <!– 表示用は通常通り –> <ApplicationDisplayVersion>$([System.DateTime]::UtcNow.ToString(‘yyyy.MM.dd.HHmm’))</ApplicationDisplayVersion> <ApplicationVersion>$(_minutes)</ApplicationVersion> </PropertyGroup>

※タグの中で改行するとたまに改行も込みでバージョン番号になってしまうことがあるので改行しないことをおすすめする。

ダウンロード用ボタン

Webサイトなどにダウンロード用のボタンを作るなら↓のジェネレーターやキットを使う。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です