【Xamarin.Forms】コンテキストメニューでListViewから行を削除する


はじめに

Xamarin.FormsのListViewで行のコンテキストメニューからその行を削除します。
コードビハインドは書かず、MVVMで実現します。

最初に言い訳です。
この記事を書いてみて、Xamarin.Fromsの基礎がわかったいないことを改めて自覚。。。
https://docs.microsoft.com/ja-jp/xamarin/xamarin-forms/app-fundamentals/
曖昧な表現がありますので、予めご了承ください。

したいこと(できたこと)

環境

Visual Studio Community 2019 for Mac
Xamarin.Forms 4.2.0
Prism.Unity.Forms 7.2.0(オプション)

ソース

GitHubにソースを上げています。
https://github.com/ats-y/TryXamarinListViewContextMenu

以下、上記ソースを抜粋して説明します。

説明

ViewとViewModelにわけて説明します。

View

MainPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             x:Class="TryContextMenu.MainPage"
             x:Name="MainPageContentPage">
    <StackLayout>

        <!-- 社員一覧 -->
        <ListView x:Name="EmployeeListView"
                  ItemsSource="{Binding EmployeeList}"
                  HasUnevenRows="True">

            <!-- 社員行 -->
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>

                        <!-- コンテキストメニューの定義-->
                        <ViewCell.ContextActions>

                            <!--
                            削除コンテキストメニュー
                            Command属性:タップするとMainPageContentPageのDeleteCommandを呼び出す。
                            CommandParameter属性:Command属性で指定したコマンドに渡すデータを指定。-->
                            <MenuItem Text="削除"
                                      IsDestructive="True"
                                      Command="{Binding Source={x:Reference MainPageContentPage}, Path=BindingContext.DeleteCommand}"
                                      CommandParameter="{Binding .}" />

                        </ViewCell.ContextActions>

                        <!-- レイアウト定義 -->
                        <StackLayout Padding="10,100,0,100"
                                     BackgroundColor="{Binding BackColor}"
                                     HorizontalOptions="FillAndExpand"
                                     Orientation="Horizontal">

                            <StackLayout Orientation="Vertical">
                                <Label Text="名前:" />
                                <Label Text="{Binding FullName}" />
                            </StackLayout>

                        </StackLayout>

                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>

        </ListView>

    </StackLayout>
</ContentPage>

Viewのポイント

コードビハインド(MainPage.xaml.cs)

今回やりたいことを実現するにあたって、コードビハインドには特に何も書いていません。

ルートContentPageの名前付け

ルートのContentPage要素には「MainPageContentPage」という名前をつけています。

   <ContentPage (略)
    x:Name="MainPageContentPage">
削除コンテキストの定義

MenuItem要素で削除コンテキストメニューを定義します。

<!--
削除コンテキストメニュー
Command属性:タップするとMainPageContentPageのDeleteCommandを呼び出す。
CommandParameter属性:Command属性で指定したコマンドに渡すデータを指定。-->
<MenuItem Text="削除"
            IsDestructive="True"
            Command="{Binding Source={x:Reference MainPageContentPage}, Path=BindingContext.DeleteCommand}"
            CommandParameter="{Binding .}" />

Command属性、CommandParameter属性でコンテキストメニューをタップした時のコマンドとコマンドに渡すパラメータを定義しています。

Command属性で指定した値によって、
MainPageContentPageに紐づいているViewModelのDeleteCommandを呼び出してね」
と定義しています。
「MainPageContentPage」はルートのContentPage要素につけた名前です。

CommandParameter属性はCommand属性で指定したコマンドを呼び出すときに渡す値を定義します。
{Binding .}とすることでタップした行データ、つまりListView要素のItemSource属性に紐づいているListのタップした行の要素が渡されました。

ViewModel

MainPageViewModel.cs
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Input;
using TryContextMenu.Models;
using Xamarin.Forms;

namespace TryContextMenu.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        /// <summary>
        /// 社員リスト
        /// </summary>
        private ObservableCollection<Employee> _employeeList;

        /// <summary>
        /// 社員リストプロパティ
        /// </summary>
        public ObservableCollection<Employee> EmployeeList
        {
            get { return _employeeList; }
            set { SetProperty(ref _employeeList, value); }
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainPageViewModel()
        {
            // 社員リストを生成する。
            _employeeList = new ObservableCollection<Employee>
            {
                new Employee { FullName = "社員 太郎" },
                new Employee { FullName = "社員 二郎" },
                new Employee { FullName = "社員 三郎" },
                new Employee { FullName = "社員 四郎" },
                new Employee { FullName = "社員 五郎" },
            };
        }

        /// <summary>
        /// 社員リストから引数で指定した社員を削除する。
        /// </summary>
        public ICommand DeleteCommand =>
            new Command<Employee>((arg) =>
        {
            Debug.WriteLine("MainPage.DeleteCommand()");

            EmployeeList.Remove(arg);
        });
    }
}

ViewModelのポイント

社員リストおよびプロパティ
MainPageViewクラスの社員リストおよびプロパティの定義部分
/// <summary>
/// 社員リスト
/// </summary>
private ObservableCollection<Employee> _employeeList;

/// <summary>
/// 社員リストプロパティ
/// </summary>
public ObservableCollection<Employee> EmployeeList
{
    get { return _employeeList; }
    set { SetProperty(ref _employeeList, value); }
}

EmployeeListプロパティは社員リストです。
MainPageViewModelクラスに対するView「MainPage.xaml」のListViewのItemSource属性で指定したバインディングItemsSource="{Binding EmployeeList}"と同じ名前にすることで、社員ListViewのリストデータとMainPageViewModel.EmployeeListが紐づきます。
これにより、MainPageViewModel.EmployeeListに入れたデータがListViewに表示されます。

今回はMainPageViewクラスのコンストラクタでEmployeeListプロパティに社員リストに社員を追加していて、実行すると追加した社員がListViewに表示されます。

MainPageViewクラスのコンストラクタ
/// <summary>
/// コンストラクタ
/// </summary>
public MainPageViewModel()
{
    // 社員リストを生成する。
    _employeeList = new ObservableCollection<Employee>
    {
        new Employee { FullName = "社員 太郎" },
        new Employee { FullName = "社員 二郎" },
        new Employee { FullName = "社員 三郎" },
        new Employee { FullName = "社員 四郎" },
        new Employee { FullName = "社員 五郎" },
    };
}

また、ObservableCollection<Employee>型にすることで、社員リストプロパティの内容が変わるとViewに反映されるようになります。

DeleteCommandコマンド
MainPageViewクラスのDeleteCommandコマンド定義部分
/// <summary>
/// 社員リストから引数で指定した社員を削除する。
/// </summary>
public ICommand DeleteCommand =>
    new Command<Employee>((arg) =>
{
    Debug.WriteLine("MainPage.DeleteCommand()");

    EmployeeList.Remove(arg);
});

削除コンテキストのMenuItem要素のCommand属性で指定したPathと同じ名前でICommand型で定義します。そして、CommandParameter属性で渡すことにした型を型引数にしています。

引数argには、削除コンテキストをタップした行のEmployeeオブジェクトが渡されます。
EmployeeList.Remove(arg);で社員リストプロパティから、argで渡されたEmployeeオブジェクトを削除します。
社員リストはObservableCollection<>型なので、これで社員ListViewから当該社員行が削除されます。