XamarinからNetMAUIへ移行中(4):ViewCell移植

,

Xamarinの時からiOSの「Segment」に対するTable用GUI部品がなかったので、CustamCellを作成して対応していた。NetMAUIではどの様に対応するか。。。のドタバタ劇です。

1)設定画面の移植:CustomCell作成

Xamarinでの画面はTableにEntryCell/TextCell/SwitchCellとCustomCellのCellSegment3~5を使用して作成していました。NetMAUIへの移行を同様にするためにCustomCellを作成する事にします。

1−1)単純なCellの構造変更

以前はCustomCellは共通部分(A)とiOS部分(B)=LayoutをiOSのLayoutSuviewで作成していました.。が、考えてみればxamlと同じ事がc# でそのまま書けるのでiOS特有のSegmentを使わないならばiOS部分は不要なので大部分を共通部分(A)として作成する事にします。

以下にコード例を載せます。

Xamarin版の場合:
<共通部分例>
    public class MarkCellBase : ViewCell
    {
        public int tagNo;
        public string[] dat;
        public string CellName;
        //
        public readonly BindableProperty _titleLabel =
            BindableProperty.Create("TitleLabel", typeof(string), 
          typeof(MarkCellBase), "");
        public string TitleLabel
        {
            get { return (string)GetValue(_titleLabel); }
            set { SetValue(_titleLabel, value); }
        }

        public MarkCellBase() : base()
        {
        }
        //
        // 保存用データリスト戻し
        public virtual (int, List<string>) getAttr()
        {
            return (0, null);
        }
        // 保存用データリスト設定
        public virtual void 
        setAttr(System.Collections.Generic.List<string> tlist)
        {
        }
        public virtual void UpdateCell()
        {
        }
        //
        protected bool SetProperty<T>(ref T backingStore, T value,
        [CallerMemberName] string propertyName = "",
        Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;

            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }
    }

    public class CellText : MarkCellBase
    {
        public string Placeholder0;
        //
        public CellText() : base()
        {
            CellName = "CText";
        }
        public static readonly BindableProperty _text0 =
            BindableProperty.Create("Text0", typeof(string), 
          typeof(CellText), "");
        public string Text0
        {
            get { return (string)GetValue(_text0); }
            set { SetValue(_text0, value); }
        }
        //
        public override void UpdateCell()
        {
            //tagNo = this.Tag;
            //
            TitleLabel = dat[2];
            //label0.Tag = 0;
            Placeholder0 = dat[3];
            //text0.Tag = 1;
            Log.Instance.Debug($"CText:UpdateCell={TitleLabel}");
        }
        // 保存用データリスト戻し
        public override (int, List<string>) getAttr()
        {
            List<string> tlist = new List<string>
            {
                Text0
            };
            if (Text0?.Length>0)
                return (1,tlist);   //修正あり
            else
                return (0, tlist);  //修正なし
        }
        // 保存用データリスト設定
        public override void setAttr(List<string> tlist)
        {
            Text0 = tlist[0];
        }
    }

<iOS固有部分例>
    public class MarkTableAddIosCellClass : UITableViewCell, 
        INativeElementView
    {
        public UILabel TitleLabel { get; set; }
        public virtual Cell Cell { get; set; }
        //
        public Element Element => Cell;

        public MarkTableAddIosCellClass()
        {
        }
        public MarkTableAddIosCellClass(UITableViewCellStyle Default,
         string cellId) : base(Default, cellId)
        { }
    }

   internal class IosCellText : MarkTableAddIosCellClass
    {
        public UITextField Text0 { get; set; }
        //
        public void init()
        {

            BackgroundColor = UIColor.SystemGray6;
            //
            SelectionStyle = UITableViewCellSelectionStyle.Gray;

            TitleLabel = new UILabel()
            {
                BackgroundColor = UIColor.Clear
            };
            Text0 = new UITextField()
            {
                BackgroundColor = UIColor.SystemGray4
            };
            //
            ContentView.Add(TitleLabel);
            ContentView.Add(Text0);
            //
            UpdateCell((CellText)Cell);
            //
            //選択が変わったら、FormのCellデータを更新する
            Text0.AddTarget((sender, e) =>
            {
                (Cell as CellText).Text0 = Text0.Text;
            }, UIControlEvent.EditingChanged);
        }
        public IosCellText(string cellId, CellText cell) : 
         base(UITableViewCellStyle.Default, cellId)
        {
            this.Cell = cell;
            init();
        }

        public IosCellText(Cell item)
        {
            this.Cell = (CellText)item;
            init();
        }
        public IosCellText()
        {
            this.Cell = null;
        }

        public void UpdateCell(CellText cell)
        {
            this.Cell = cell;
            CellText lcell = (CellText)Cell;
            TitleLabel.Text = lcell.TitleLabel;
            Text0.Text = lcell.Text0;
            //
            Text0.Placeholder = lcell.Placeholder0;
            //
            Text0.Tag = 0;
        }

        public override void LayoutSubviews()
        {
            base.LayoutSubviews();
            //
            if (TitleLabel == null)
                return;
            TitleLabel.Frame = new CGRect(15, 5, 
           ContentView.Bounds.Width - 30, 25);
            Text0.Frame = new CGRect(15, 30, 
           ContentView.Bounds.Width - 30, 25);
        }
        //
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                TitleLabel.Dispose();
                Text0.Dispose();
                Cell = null;
            }
            base.Dispose(disposing);
        }
    }

    public class IosCellTextRenderer : ViewCellRenderer
    {
        public override UIKit.UITableViewCell 
       GetCell(Cell item, UIKit.UITableViewCell reusableCell, 
             UIKit.UITableView tv)
        {
            var cellText = reusableCell as IosCellText;
            if (cellText == null)
            {
                //リサイクルでなければセルを生成する
                cellText = new IosCellText(item);
            }
            else
            {
                //セル内容の更新
                cellText.UpdateCell((CellText)item);
            }
            return cellText;
        }
    }

構造を変えた事で、iOS固有のViewCellRendererが不要になり随分すっきりしました。

NetMAUIの構造変更版:機能が全く同じでは無いので構造の参考として見て下さい
<共通部分例>
    public class MarkCellBase : ViewCell
    {
        public Label _titleLabel = new Label { Text = "TitleLabel", 
          Padding = new Thickness(20, 0, 0, 0) };
        //
        public int tagNo;
        public string[] dat;
        public string CellName;
        //
        public readonly BindableProperty TitleLabelProperty =
            BindableProperty.Create("TitleLabel", typeof(string), 
          typeof(MarkCellBase),   default(string));

        public string TitleLabel
        {
            get { return (string)GetValue(TitleLabelProperty); }
            set { SetValue(TitleLabelProperty, value); }
        }

        public MarkCellBase() : base()
        {
            //ここでは何もしない
        }
        //
        // 保存用データリスト戻し
        public virtual (int, List<string>) getAttr()
        {
            return (0, null);
        }
        // 保存用データリスト設定
        public virtual void 
        setAttr(System.Collections.Generic.List<string> tlist)
        {
        }
        public virtual void UpdateCell()
        {
        }
        //
        protected bool SetProperty<T>(ref T backingStore, T value,
        [CallerMemberName] string propertyName = "",
        Action? onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;

            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }
    }

    public class CellText : MarkCellBase
    {
        public string Placeholder0;
        public Entry _entry0 = new Entry { Text = "Entry0" };
        //
        public CellText() : base()
        {
            CellName = "CText";
            var stk = new StackLayout();
            stk.Orientation = StackOrientation.Horizontal;
            stk.Add(_Lable);
            stk.Add(_entry0);
            View = stk;
            //
            Appearing += OnAppearing;
        }
        private void OnAppearing(object sender, EventArgs e)
        {
            Console.WriteLine("CellText");
            _Lable.Text = TitleLabel;
            _entry0.Text = Text0;
            //
        }
        public static readonly BindableProperty _text0 =
            BindableProperty.Create("Text0", typeof(string), 
           typeof(CellText), "");
        public string Text0
        {
            get { return (string)GetValue(_text0); }
            set { SetValue(_text0, value); }
        }
        public override void UpdateCell()
        {
            //
            TitleLabel = dat[2];
            Placeholder0 = dat[3];
            Log.Debug($"CText:UpdateCell={TitleLabel}");
        }
        // 保存用データリスト戻し
        public override (int, List<string>) getAttr()
        {
            List<string> tlist = new List<string>
            {
                Text0
            };
            if (Text0?.Length>0)
                return (1,tlist);   //修正あり
            else
                return (0, tlist);  //修正なし
        }
        // 保存用データリスト設定
        public override void setAttr(List<string> tlist)
        {
            Text0 = tlist[0];
        }
    }

1−2)SegmentCellの移植

iOSのUISegmentedControlを使うためにCustomCellを作成していましたが、必要な数のButtonを配置して、選択状態を自分で管理するならば共通部分のみで実現できると考えて以下の様にします。

Xamarin版の場合:
<共通部分例>
    public class MarkCellBase : ViewCell
    {
     。。。。。。
    }

   public class CellSegment2 : MarkCellBase
    {
        //
        public CellSegment2() : base()
        {
            CellName = "CSeg2";
        }
        //
        public static readonly BindableProperty _selectedSegment =
            BindableProperty.Create("SelectedSegment", typeof(int), 
            typeof(CellSegment2), 0);
        public int SelectedSegment
        {
            get { return (int)GetValue(_selectedSegment); }
            set { SetValue(_selectedSegment, value); }
        }
        //
        public static readonly BindableProperty _text0 =
            BindableProperty.Create("Text0", typeof(string), 
            typeof(CellSegment2), "");
        public string Text0
        {
            get { return (string)GetValue(_text0); }
            set { SetValue(_text0, value); }
        }
        public static readonly BindableProperty _text1 =
            BindableProperty.Create("Text1", typeof(string), 
            typeof(CellSegment2), "");
        public string Text1
        {
            get { return (string)GetValue(_text1); }
            set { SetValue(_text1, value); }
        }
        public override void UpdateCell()
        {
            TitleLabel = dat[2];
            Text0 = dat[3];
            Text1 = dat[4];
            Log.Instance.Debug($"CSeg2:UpdateCell={TitleLabel}");
        }
        // 保存用データリスト戻し
        public override (int, List<string>) getAttr()
        {
            string title = "";
            if (SelectedSegment == 0)
                title = Text0;
            else if (SelectedSegment == 1)
                title = Text1;
            //
            List<string> tlist = new List<string>
            {
		$"{SelectedSegment}",
                title
            };
            if (SelectedSegment > 0)
                return (1, tlist);   //修正あり
            else
                return (0, tlist);  //修正なし
        }
        // 保存用データリスト設定
        public override void setAttr(List<string> tlist)
        {
            SelectedSegment = int.Parse(tlist[0]);
        }
    }

<iOS固有部分例>
    public class MarkTableAddIosCellClass : UITableViewCell, 
        INativeElementView
    {
      。。。。
    }

   internal class IosCellSegment2 : MarkTableAddIosCellClass
    {
        public UISegmentedControl Segment0 { get; set; }
        //
        public void init()
        {
            Cell.PropertyChanged += CellPropertyChanged;
            BackgroundColor = UIColor.SystemGray6;
            //
            SelectionStyle = UITableViewCellSelectionStyle.Gray;
            //ContentView.BackgroundColor = 
              UIColor.FromRGB(255, 255, 224);
            TitleLabel = new UILabel()
            {
                BackgroundColor = UIColor.Clear
            };
            CellSegment2 lcell = (CellSegment2)Cell;
            Segment0 = new UISegmentedControl()
            {
                SelectedSegment = lcell.SelectedSegment,
                Tag = 1,
                BackgroundColor = UIColor.Clear
            };
            //
            ContentView.Add(TitleLabel);
            ContentView.Add(Segment0);
            //
            UpdateCell(lcell);
            //選択が変わったら、FormのCellデータを更新する
            Segment0.ValueChanged += (s, a) => {
                var index = Segment0.SelectedSegment;
                lcell.SelectedSegment = (int) index;
            };
        }

        public IosCellSegment2(string cellId, CellSegment2 cell) : 
            base(UITableViewCellStyle.Default, cellId)
        {
            this.Cell = cell;
            init();
        }
        public IosCellSegment2(Cell item)
        {
            this.Cell = (CellSegment2)item;
            init();
        }
        public IosCellSegment2()
        {
            this.Cell = null;
        }
        public void UpdateCell(CellSegment2 cell)
        {
            this.Cell = cell;
            TitleLabel.Text = cell.TitleLabel;
            Segment0.RemoveAllSegments();
            Segment0.InsertSegment(cell.Text0, 0, false);
            Segment0.InsertSegment(cell.Text1, 1, false);
            Segment0.SelectedSegment = cell.SelectedSegment;
        }
        public override void LayoutSubviews()
        {
            base.LayoutSubviews();
            //
            if (TitleLabel == null)
                return;
            TitleLabel.Frame = new CGRect(15, 5, 
            ContentView.Bounds.Width - 30, 25);
            Segment0.Frame = new CGRect(15, 30, 
            ContentView.Bounds.Width - 30, 25);
        }
        //ProperyChanged対応
        public void CellPropertyChanged(object sender, 
           PropertyChangedEventArgs e)
        {
            CellSegment2 lcell = (CellSegment2)Cell;
            if (e.PropertyName == "TitleLabel")
            {
                TitleLabel.Text = lcell.TitleLabel;
            }
            else if (e.PropertyName == "Segment0")
            {
                Segment0.SelectedSegment = lcell.SelectedSegment;
            }
        }
        //
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                TitleLabel.Dispose();
                Segment0.Dispose();
                Cell.PropertyChanged -= CellPropertyChanged;
                Cell = null;
            }
            base.Dispose(disposing);
        }
    }

    public class IosCellSegment2Renderer : ViewCellRenderer 
    {
        public override UIKit.UITableViewCell GetCell(Cell item, 
           UIKit.UITableViewCell reusableCell, 
           UIKit.UITableView tv)
        {
            var cellSegment2 = reusableCell as IosCellSegment2;
            if (cellSegment2 == null)
            {
                //リサイクルでなければセルを生成する
                cellSegment2 = new IosCellSegment2(item);
            }
            else
            {
                //リサイクル前のFormsCellのPropertyChangedを解除する
                cellSegment2.Cell.PropertyChanged -= 
              cellSegment2.CellPropertyChanged;
                //セル内容の更新
                cellSegment2.UpdateCell((CellSegment2)item);
            }
            //リサイクル後のFormsCellのPropertyChangedを購読する
            item.PropertyChanged += cellSegment2.CellPropertyChanged;
            return cellSegment2;
        }
    }

構造を変えた事で、iOS固有のViewCellRendererが不要になり随分すっきりしました。

NetMAUIの構造変更版:機能が全く同じでは無いので構造の参考として見て下さい
<共通部分例>
    public class MarkCellBase : ViewCell  
   {  
        。。。。。。    
  }
    public class CellSegment2 : MarkCellBase
    {
        public Button _button0 = new Button { Text = "Text0", 
          Padding = new Thickness(20, 0, 0, 0) };
        public Button _button1 = new Button { Text = "Text1" };
        //
        public CellSegment2() : base()
        {
            CellName = "CSeg2";
            var stkV = new StackLayout();
            stkV.Add(_Lable);
            //
            var stkH = new StackLayout();
            stkH.Orientation = StackOrientation.Horizontal;
            stkH.Add(_button0);
            stkH.Add(_button1);
            //
            stkV.Add(stkH);
            View = stkV;
            //
            Appearing += OnAppearing2;
        }
        private void OnAppearing2(object sender, EventArgs e)
        {
            Console.WriteLine("CellSegment2");
            // 
            _Lable.Text = TitleLabel;
            _button0.Text = Text0;
            _button1.Text = Text1;
            //
            _button0.Clicked += OnButtonCliced2;
            _button1.Clicked += OnButtonCliced2;
            //
            setButtonColor2();
        }
        private void setButtonColor2()
        {
            if (SelectedSegment != 0) _button0.BackgroundColor = 
                   Colors.White;
            else _button0.BackgroundColor = Colors.LightCyan;
            if (SelectedSegment != 1) _button1.BackgroundColor = 
                   Colors.White;
            else _button1.BackgroundColor = Colors.LightCyan;
        }
        private void OnButtonCliced2(object sender, EventArgs e)
        {
            Button btn = (Button)sender;
            btn.BackgroundColor = Colors.Gray;
            if (btn == _button0) SelectedSegment = 0;
            if (btn == _button1) SelectedSegment = 1;
            setButtonColor2();
        }
        //
        public static readonly BindableProperty _selectedSegment =
            BindableProperty.Create("SelectedSegment", typeof(int), 
           typeof(CellSegment2), 0);
        public int SelectedSegment
        {
            get { return (int)GetValue(_selectedSegment); }
            set { SetValue(_selectedSegment, value); }
        }
        //
        public static readonly BindableProperty _text0 =
            BindableProperty.Create("Text0", typeof(string), 
           typeof(CellSegment2), "");
        public string Text0
        {
            get { return (string)GetValue(_text0); }
            set { SetValue(_text0, value); }
        }
        public static readonly BindableProperty _text1 =
            BindableProperty.Create("Text1", typeof(string), 
           typeof(CellSegment2), "");
        public string Text1
        {
            get { return (string)GetValue(_text1); }
            set { SetValue(_text1, value); }
        }
        public override void UpdateCell()
        {
            //tagNo = this.Tag;
            TitleLabel = dat[2];
            //label0.Tag = 0;
            //segment0.SelectedSegment = 0;
            //segment0.SetTitle(dat[3], 0);
            Text0 = dat[3];
            //segment0.SetTitle(dat[4], 1);
            Text1 = dat[4];
            //segment0.Tag = 1;
            Log.Debug($"CSeg2:UpdateCell={TitleLabel}");
        }
        // 保存用データリスト戻し
        public override (int, List<string>) getAttr()
        {
            string title = "";
            if (SelectedSegment == 0)
                title = Text0;
            else if (SelectedSegment == 1)
                title = Text1;
            //
            List<string> tlist = new List<string>
            {
		$"{SelectedSegment}",
                title
            };
            if (SelectedSegment > 0)
                return (1, tlist);   //修正あり
            else
                return (0, tlist);  //修正なし
        }
        // 保存用データリスト設定
        public override void setAttr(List<string> tlist)
        {
            SelectedSegment = int.Parse(tlist[0]);
            setButtonColor2();
        }
    }

2)属性入力画面の表示例

2024/9月初め:属性入力画面は同じ様な画面になりました。

3)動作設定画面:ゴースト発生

属性入力画面と異なり、 動作設定画面では次の図のようなゴーストが発生しています(=右画面の「測定回数」や「測定間隔」「測定状態」が、有効期限や操作説明書などと重なって表示される)。その他でも画面を上下にスクロールすると表示が消えたりします。

簡単に言えば「NetMAUIのバグ」でしょうが、Tableを使って固定的なCell配置を行っているので、使い方としてあまり良く無いのは確かです(=それでもXamarinの時は問題なく動いていたので、NetMAUIのバグでしょう。Netの記事を見るとNetMAUIはまだまだバグが多い様です。また、現在使用しているのは開発環境の制約でNet7.0なのも問題かもしれません)。

ゴースト発生状況

3-1)ゴースト対策(1):全てXAMLで作成

考えてみればCustomCellを作って色々な設定を表現している訳ですが、全てXAMLで代用できる筈なのでXAMLで全て書き換えてみました。

が、結果は同じようなゴーストが発生してダメでした。

3-2)ゴースト対策(2):Tableの使用を止める

Tableの使い方として大部分は同一形式データをリストの様に使うのと、表示時に毎回Cellを作成するのが本道なので、最初に1度だけ、色々な形式データを作成する使い方は、NetMAUIでは想定外の可能性がありテストも間に合っていないのでしょう。

なので、動作設定画面はTableではなく、StackLayoutで全て作り直す事にします。
Tableで使用する部品はViewcellを継承して作りますが、StackLayoutで使用する部品なのでContentViewを継承して作成します。

以下のコード例の様に、CustomViewCellをベースにContentView用に変えていきました。

NetMAUI:ViewCellからContentViewに変更:
<共通部分例>
    public class SettingPartsBase : ContentView
    {
        public Label _titleLabel = new Label { Text = "TitleLabel", 
         Padding = new Thickness(0, 0, 0, 0) };
        //------------------------------------------
        //  TitleLabel-->TitleLabelProperty はxmlとのbind名称に自動的になる
        public readonly BindableProperty TitleLabelProperty =
            BindableProperty.Create(nameof(TitleLabel), typeof(string),
          typeof(MarkCellBase), default(string));
        public string TitleLabel
        {
            get { return (string)GetValue(TitleLabelProperty); }
            set { SetValue(TitleLabelProperty, value); }
        }
        //------------------------------------------
        public SettingPartsBase() : base()
        {
            //ここでは何もしない
        }
    }

    public class SettingPartsSegment2 : SettingPartsBase
    {
        public Button _button0 = new Button { Text = "Text0" };
        public Button _button1 = new Button { Text = "Text1" };
        //-----------------------------------------
        public static readonly BindableProperty SelectedSegmentProperty =
            BindableProperty.Create(nameof(SelectedSegment), typeof(int),
             typeof(SettingPartsSegment2), default(int));
        public int SelectedSegment
        {
            get { return (int)GetValue(SelectedSegmentProperty); }
            set { SetValue(SelectedSegmentProperty, value); }
        }
        public static readonly BindableProperty _text0 =
            BindableProperty.Create(nameof(Text0), typeof(string), 
             typeof(SettingPartsSegment2), default(string));
        public string Text0
        {
            get { return (string)GetValue(_text0); }
            set { SetValue(_text0, value); }
        }
        public static readonly BindableProperty _text1 =
            BindableProperty.Create(nameof(Text1), typeof(string),
          typeof(SettingPartsSegment2), default(string));
        public string Text1
        {
            get { return (string)GetValue(_text1); }
            set { SetValue(_text1, value); }
        }
        //-----------------------------------------
        public SettingPartsSegment2() : base()
        {
            var stkV = new StackLayout();
            stkV.Add(_titleLabel);
            //
            var stkH = new StackLayout() { Padding =
                 new Thickness(10, 0, 0, 0) };
            stkH.Orientation = StackOrientation.Horizontal;
            stkH.Add(_button0);
            stkH.Add(_button1);
            //
            stkV.Add(stkH);
            this.Content = stkV;
            //
            this.Loaded += OnAppearing2;
        }

        private void OnAppearing2(object? sender, EventArgs e)
        {
            Console.WriteLine("CellSegment2");
            _titleLabel.Text= TitleLabel;
            _button0.Text = Text0;
            _button1.Text = Text1;
            //
            int baseWidth = App.Width - 60;
            _button0.WidthRequest = baseWidth / 2;
            _button1.WidthRequest = _button0.WidthRequest;
            //
            _button0.Clicked += OnButtonCliced2;
            _button1.Clicked += OnButtonCliced2;
            //
            setButtonColor2();
        }
        private void setButtonColor2()
        {
            if (SelectedSegment != 0) 
           _button0.BackgroundColor = Colors.White;
            else 
           _button0.BackgroundColor = Colors.LightCyan;
            if (SelectedSegment != 1) 
           _button1.BackgroundColor = Colors.White;
            else _button1.BackgroundColor = Colors.LightCyan;
        }
        private void OnButtonCliced2(object? sender, EventArgs e)
        {
            if (sender is null)
                return;
            Button btn = (Button)sender;
            btn.BackgroundColor = Colors.Gray;
            if (btn == _button0) SelectedSegment = 0;
            if (btn == _button1) SelectedSegment = 1;
            setButtonColor2();
        }
    }

5)動作設定画面表示例

2024/9月初め:動作設定画面は同じ様な画面になりました(随分コンパクトになりました)。

動作設定画面ーtop
動作設定画面-middle
動作設定画面-buttom



コメントを残す

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

PAGE TOP