C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[5]: ユーザーコントロールの使い方


(過去ログ 155 を表示中)

[トピック内 6 記事 (1 - 6 表示)]  << 0 >>

■90068 / inTopicNo.1)  ユーザーコントロールの使い方
  
□投稿者/ ルパン (23回)-(2019/02/04(Mon) 10:24:15)

分類:[.NET 全般] 

VisualStudio C#
Windows10
にて、
以下のようなプログラム(重要部分のみ表示)を作成しています。

フォーム上にはユーザコントロールを切り替えて表示するように
ユーザーコントロール呼び出し用のボタンがあり、
ユーザーコントロール上にはラベル、ボタン(終了)など
他に複数のコントロールが配置しています。

ユーザーコントロールのコントロールボックスの×ボタンや
終了ボタンを押した場合はlblTime.Text の値が保存されるのですが、

フォーム上のユーザーコントロール呼び出しボタンを押すと
lblTime.Text が "" になって保存されてしまいます。

どうすればよいのでしょうか?


≡呼び出し側

private void Button_Click(object sender, EventArgs e)
{
 foreach (Control cnt in pnlSub.Controls)
 {
  pnlSub.Controls.Remove(cnt);
  cnt.Dispose();
 }
 if (SelectButton == btnTime) AddUserControl(new ucTime());
}

private void AddUserControl(Control cnt)
{
 cnt.Dock = DockStyle.Fill;
 pnlSub.Controls.Add(cnt);
}

≡ユーザーコントロール
private void ucTime_Disposed(object sender, EventArgs e)
{
 Properties.Settings.Default.ucTime = lblTime.Text;
 Properties.Settings.Default.Save();
}

private void btnClose_Click(object sender, EventArgs e)
{
 this.ParentForm.Close();
}

引用返信 編集キー/
■90069 / inTopicNo.2)  Re[1]: ユーザーコントロールの使い方
□投稿者/ 魔界の仮面弁士 (2036回)-(2019/02/04(Mon) 10:43:36)
2019/02/04(Mon) 11:21:16 編集(投稿者)

No90068 (ルパン さん) に返信
> ユーザーコントロール上にはラベル、ボタン(終了)など
> 他に複数のコントロールが配置しています。
親子関係がどうなっているのか読み取りきれませんでした…。
コードから察すると、そこまでに登場した UI 部品としては、
 ・Form
 ・Button イベントハンドラに割り当てられた Button コントロール
 ・SelectButton フィールド変数に割り当てられた Button コントロール
 ・終了ボタン
 ・btnTime (Button コントロール)
 ・pnlSub (Panel コントロール)
 ・ucTime (UserControl コントロール)
 ・lblTime (Label コントロール)
などがありそうですが。


> foreach (Control cnt in pnlSub.Controls)
> {
>  pnlSub.Controls.Remove(cnt);
>  cnt.Dispose();
> }

コレクションのループ中に、そのコレクション自身の要素を増減させると
カウンターがずれるので避けてください。

回避策としては、たとえば .ToArray() で配列にコピーしてから、
それを列挙するという手が使えます。あるいは全削除なら、
while ループで空になるまで繰り返し [0] 番を削っていくという手も。


> private void ucTime_Disposed(object sender, EventArgs e)
> {
>  Properties.Settings.Default.ucTime = lblTime.Text;

上記の処理ですが、処理順などを考慮した上で、
ucTime が Diposed となったときに、
lblTime がまだ Disposed 状態でないことは
確実に保証できているのでしょうか。
引用返信 編集キー/
■90070 / inTopicNo.3)  Re[2]: ユーザーコントロールの使い方
□投稿者/ ルパン (24回)-(2019/02/04(Mon) 11:25:36)
No90069 (魔界の仮面弁士 さん) に返信
ありがとうございます。

削除部分は以下のように変更しました。

>ucTiem が Diposed となったときに、lblTime がまだ Disposed 状態でないことは
>保証できているのでしょうか。
保証できるようにするにはどうすればよいのでしょうか?

フォームでは FormClose に入れていたのですが
ユーザーコントロールではこれに該当するものがわからなかったので
ネットで調べたら 「UserControl の後処理は Dispose で行う」とあったので、
ユーザーコントロールのコンストラクタに
this.Disposed += UcMakeAbilityTime_Disposed;
を追加し Dispose のイベントに Properties への保存処理を入れました。

現状以下のようになっています。

frmMain.cs              ucTime.cs
┌──────┬─────────┐  ┌─────────┐
│┌────┐│         │  │┌──┐┌───┐│
││btnTime ││         │  ││lbl ││btn  ││
│└────┘│         │  ││Time││Close ││
│      │         │  │└──┘└───┘│
│pnlMain   │pnlSub      │  │UserControl    │
└──────┴─────────┘  └─────────┘

≡ frMain.cs ≡
private void Button_Click(object sender, EventArgs e)
{
 SelectButton((Control)sender);
}

private void SelectButton(Control SelectButton)
{
 while (pnlSub.Controls.Count > 0){
  pnlSub.Controls.Remove(pnlSub.Controls[0]);
  pnlSub.Controls[0].Dispose();
 }
 if (sender== btnTime) AddUserControl(new ucTime());
}

private void AddUserControl(Control cnt)
{
 cnt.Dock = DockStyle.Fill;
 pnlSub.Controls.Add(cnt);
}

≡ ucTime.cs ≡
public ucTime()
{
 InitializeComponent();
 this.Disposed += UcTime_Disposed;
}

private void UcMakeAbilityTime_Disposed(object sender, EventArgs e)
{
 Properties.Settings.Default.ucMakeAbilityTime = lblTime.Text;
 Properties.Settings.Default.Save();
}

private void btnClose_Click(object sender, EventArgs e)
{
 this.ParentForm.Close();     ←正しいかどうかわかりませんが、
}                  この場合は正常に保存されている。

引用返信 編集キー/
■90071 / inTopicNo.4)  Re[3]: ユーザーコントロールの使い方
□投稿者/ 魔界の仮面弁士 (2037回)-(2019/02/04(Mon) 13:47:10)
2019/02/04(Mon) 14:32:50 編集(投稿者)

No90070 (ルパン さん) に返信
>   pnlSub.Controls.Remove(pnlSub.Controls[0]);
>   pnlSub.Controls[0].Dispose();

そのコードは正しくありません。
1 行目が指している [0] と 2 行目が指している [0] が
別のコントロールを指していることに注意してください。


提示のコードだと、pnlSub 上の子コントロールが【奇数個】の場合に
最後の 1 個が Remove されないことになってしまいます。
※例外:ArgumentOutOfRangeException

また、【偶数個】であった場合、一見するとすべて Remove されるように
見えますが、実際には偶数・奇数いずれの場合にも、
「半数は Remove しただけ(Dispose していない)」
「半数は Dispose しただけ(自動的に Remove される)」
という動作に陥っていることに注意してください。



代案1:削除対象のコレクションと列挙用のコレクションを分ける

Control[] children = pnlSub.Controls.OfType<Control>().ToArray();
foreach (Control c in children)
{
 pnlSub.Controls.Remove(c);
 c.Dispose();
}


代案2:子アイテムを変数等に保持しておき、コレクションから除去後に破棄する
while (pnlSub.Controls.Count > 0)
{
 using (pnlSub.Controls[0])
 {
  pnlSub.Controls.RemoveAt(0);
 }
}


代案3:後ろから前に Dispose する
for (int i = pnlSub.Controls.Count - 1; i >= 0; i--)
{
 // Dispose されると自動的に Remove される
 pnlSub.Controls[i].Dispose();
}



> ネットで調べたら 「UserControl の後処理は Dispose で行う」とあったので、

親コントロールが破棄される際には、それに先んじて
子コントロールが破棄されますので、Disposed イベント中には
他のコントロール(もちろん Label にも)アクセスすることはできません。

故にたとえば Dispose 時には、「子コントロールそのもの」に
アクセスするのではなく、予め保持しておいた
「子コントロールが持っていた値」を使って処理するようにします。


新規プロジェクトに下記を貼り、実行してフォームを閉じてみてください。


using System.Diagnostics;
public partial class Form1 : Form
{
 private UserControl uc;
 public Form1()
 {
  InitializeComponent();
  Controls.Add(uc = new UC() { Dock = DockStyle.Fill });
 }
}

public class UC : UserControl
{
 private Label lbl;
 private string _labelText = null;
 private string LabelText { get { return _labelText; } }
 public UC()
 {
  Controls.Add(lbl = new Label() { Text = "lbl", Dock = DockStyle.Fill });
  
  // Label が破棄された後でも Text を拾えるよう、保持しておく
  lbl.TextChanged += delegate { _labelText = lbl.Text; };
  _labelText = lbl.Text;

  //
  lbl.Disposed += delegate
  {
   Debug.WriteLine("Label.Disposed");
   Debug.WriteLine(" UserControl.Disposing = " + this.Disposing);
   Debug.WriteLine(" UserControl.IsDisposed = " + this.IsDisposed);
   Debug.WriteLine(" Label.Disposing = " + lbl.Disposing);
   Debug.WriteLine(" Label.IsDisposed = " + lbl.IsDisposed);
   Debug.WriteLine(" Label.Text = [" + lbl.Text + "]");
   Debug.WriteLine(" LabelText = [" + this.LabelText + "]");
  };
  this.Disposed += delegate
  {
   Debug.WriteLine("UserControl.Disposed");
   Debug.WriteLine(" UserControl.Disposing = " + this.Disposing);
   Debug.WriteLine(" UserControl.IsDisposed = " + this.IsDisposed);
   Debug.WriteLine(" Label.Disposing = " + lbl.Disposing);
   Debug.WriteLine(" Label.IsDisposed = " + lbl.IsDisposed);
   Debug.WriteLine(" Label.Text = [" + lbl.Text + "]");
   Debug.WriteLine(" LabelText = [" + this.LabelText + "]");
  };
 }
}


---- 実行結果 ----
Label.Disposed
 UserControl.Disposing = True
 UserControl.IsDisposed = False
 Label.Disposing = True
 Label.IsDisposed = False
 Label.Text = []
 LabelText = [lbl]
UserControl.Disposed
 UserControl.Disposing = True
 UserControl.IsDisposed = False
 Label.Disposing = False
 Label.IsDisposed = True
 Label.Text = []
 LabelText = [lbl]
引用返信 編集キー/
■90072 / inTopicNo.5)  Re[4]: ユーザーコントロールの使い方
□投稿者/ ルパン (25回)-(2019/02/04(Mon) 15:25:16)
No90071 (魔界の仮面弁士 さん) に返信

Dispose すると
配置されているコントロールのデータは破棄されているが
プライベートで指定している各データは消えないんということですか?
つまり、Dispose イベント内では完全にユーザーコントロールのデータが
破棄されているわけではないということでいいのでしょうか?
イベントを抜けた後に完全破棄されるということですね?



引用返信 編集キー/
■90074 / inTopicNo.6)  Re[5]: ユーザーコントロールの使い方
□投稿者/ 魔界の仮面弁士 (2038回)-(2019/02/04(Mon) 16:57:35)
2019/02/04(Mon) 20:03:33 編集(投稿者)

No90072 (ルパン さん) に返信
> Dispose すると
> 配置されているコントロールのデータは破棄されているが

コントロールのデータというよりは、コントロールそのものの破棄です。

繰り返しになりますが、Control クラスにおいては、
Dispose された際に、Controls プロパティに登録されている
子コントロール群も Dispose されるように設計されています。
(子がさらに子を持っていた場合も同様に連鎖します)

このため UserControl の Disposed 時には、子 Label が既に処分されており、
Label の Text プロパティが "" を返してしまうわけです。


いずれにせよ、UserControl や Form や Panel 等の Disposed イベントが
呼ばれたときには、それらの Controls に追加されていた子孫コントロールは
「既に処分されていて利用不可能な状態」であるものとして扱うべきです。


たとえば No90071 でお見せした LabelText プロパティを例に挙げると、
 string LabelText { get { return this._labelText; } }
という実装の場合には、Dispose された後でも利用できますが、もしもこれを
 string LabelText { get { return this.lbl.Text; } }
にしていた場合は、Dispose 後にはアクセスすべきでは無いということです。

lbl 変数(Label クラス)は IDisposable なオブジェクトですが、
_labelText 変数(String クラス) は IDisposable なオブジェクトでは無いわけで。


> プライベートで指定している各データは消えないんということですか?

Dispose された場合、そのクラスが管理していた情報のうち、
アンマネージなオブジェクトは既に破棄されている状態になります。
一方、マネージリソースの破棄についてはオプション扱いなのでケースバイケースです。

また、幾つかのメンバーについては、破棄後にアクセスした場合に
ObjectDisposedException の例外になるように設計されています。
(たとえば Handle プロパティなど)



参考までに Text プロパティの場合、CacheText フラグの付与有無でも動作が異なります。
Label や TextBox 等の場合、Dispose 直後は "" が返却されるようになりますが、
Button、TabPage、ToolStripPanel 等では、Dispose 直前の値が利用されます。

CacheText の有無でどのように変化するか、実験コードを載せておきます。
下記を実行すると、
  FormClosing:あいうえお/かきくけこ
  Disposed:/かきくけこ
という結果になることを確認できます。


public partial class Form1 : Form
{
 private Class1 label1;
 private Class2 label2;
 public Form1()
 {
  InitializeComponent();
  Controls.Add(label1 = new Class1() { Text = "あいうえお", Top = 30 });
  Controls.Add(label2 = new Class2() { Text = "かきくけこ", Top = 90 });

  this.FormClosing += delegate { System.Diagnostics.Debug.WriteLine("FormClosing:" + label1.Text + "/" + label2.Text); };
  this.Disposed += delegate { System.Diagnostics.Debug.WriteLine("Disposed:" + label1.Text + "/" + label2.Text); };
 }
 private class Class1 : Label { public Class1() { SetStyle(ControlStyles.CacheText, false); } }
 private class Class2 : Label { public Class2() { SetStyle(ControlStyles.CacheText, true); } }
}


> つまり、Dispose イベント内では

「Dispose イベント」ではなく、過去形表記の
「Disposed イベント」ですね。



> プライベートで指定している各データは消えないんということですか?

private か public かは関係ないですよ。
消えるかどうかは開発者の実装次第です。IDisposable でないオブジェクトは特に。


.NET Framework 自身が提供しているクラス(Label や UserControl) 自体が持つメンバーではなく、
それらを継承したクラス(あるいは自作の class 等)に対して追加したメンバーについては、
Dispose 時にそのデータを処分するかどうかも、開発者自身が決める必要があります。

もし Dispose された時に自作したメンバーの情報も処分する必要があるのなら、
そのコントロールの protected な『void Dispose(bool)』メソッドを
オーバーライドする必要があります。(あるいは Disposed イベントを利用する)
引用返信 編集キー/


トピック内ページ移動 / << 0 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -