以下は、オブジェクトの状態を定義する列挙型と、この列挙型の実装を示すクラスの簡単な例です。
public enum StatusEnum
{
Clean = 0,
Dirty = 1,
New = 2,
Deleted = 3,
Purged = 4
}
public class Example_Class
{
private StatusEnum _Status = StatusEnum.New;
private long _ID;
private string _Name;
public StatusEnum Status
{
get { return _Status; }
set { _Status = value; }
}
public long ID
{
get { return _ID; }
set { _ID = value; }
}
public string Name
{
get { return _Name; }
set { _Name = value; }
}
}
クラスオブジェクトにデータベースのデータを入力するときは、列挙値を「clean」に設定します。ほとんどのロジックをプレゼンテーション層から除外することを目的として、プロパティが変更されたときに列挙値を「ダーティ」に設定するにはどうすればよいですか。
私は次のようなことを考えていました。
public string Name
{
get { return _Name; }
set
{
if (value != _Name)
{
_Name = value;
_Status = StatusEnum.Dirty;
}
}
}
クラスの各プロパティのセッターで。
これは良い考えのように聞こえますか、プレゼンテーション層でそれを行わずにダーティフラグを割り当てる方法について誰かがより良い考えを持っていますか?.
クラスレベルでダーティフラグ(または、さらに言えば、通知)が本当に必要な場合は、以下のようなトリックを使用して、プロパティの乱雑さを最小限に抑えることができます(ここではIsDirty
とPropertyChanged
、楽しみのためだけに)。
明らかに、列挙型アプローチを使用するのは簡単なことです(私がしなかった唯一の理由は、例を単純に保つことでした):
class SomeType : INotifyPropertyChanged {
private int foo;
public int Foo {
get { return foo; }
set { SetField(ref foo, value, "Foo"); }
}
private string bar;
public string Bar {
get { return bar; }
set { SetField(ref bar, value, "Bar"); }
}
public bool IsDirty { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void SetField<T>(ref T field, T value, string propertyName) {
if (!EqualityComparer<T>.Default.Equals(field, value)) {
field = value;
IsDirty = true;
OnPropertyChanged(propertyName);
}
}
protected virtual void OnPropertyChanged(string propertyName) {
var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
その一部を抽象基本クラスにプッシュすることもできますが、それは別の議論です
1つのオプションは、書き込み時に変更することです。もう1つは、すべての元の値のコピーを保持し、誰かがそれを要求したときに汚れを計算することです。これには、正確にわかるという追加の利点がありますwhichフィールドが変更されました(そしてどのように)。つまり、最小限の更新ステートメントを発行して、マージの競合解決を少し簡単にすることができます。
また、すべてのダーティチェックを1か所にまとめることができるため、コードの残りの部分を汚染することはありません。
完璧だと言っているわけではありませんが、検討する価値のあるオプションです。
この方法で実装し、コードの量を減らしたい場合は、アスペクト指向プログラミングの適用を検討してください。
たとえば、 PostSharp のようなコンパイル時ウィーバーを使用して、プロパティに適用できる「アスペクト」を作成できます。この側面により、適切な場合にダーティフラグが設定されていることを確認します。
アスペクトは次のようになります。
[Serializable]
[AttributeUsage(AttributeTargets.Property)]
public class ChangeTrackingAttribute : OnMethodInvocationAspect
{
public override void OnInvocation( MethodInvocationEventArgs e )
{
if( e.Delegate.Method.ReturnParameter.ParameterType == typeof(void) )
{
// we're in the setter
IChangeTrackable target = e.Delegate.Target as IChangeTrackable;
// Implement some logic to retrieve the current value of
// the property
if( currentValue != e.GetArgumentArray()[0] )
{
target.Status = Status.Dirty;
}
base.OnInvocation (e);
}
}
}
つまり、ChangeTrackingを実装するクラスは、少なくとも 'Status'プロパティを持つIChangeTrackable
インターフェイス(カスタムインターフェイス)を実装する必要があります。
カスタム属性ChangeTrackingProperty
を作成し、上記で作成したアスペクトがこのChangeTrackingProperty
属性で装飾されたプロパティにのみ適用されるようにすることもできます。
例えば:
public class Customer : IChangeTrackable
{
public DirtyState Status
{
get; set;
}
[ChangeTrackingProperty]
public string Name
{ get; set; }
}
これは私がそれを見る方法の少しです。 PostSharpがコンパイル時に、ChangeTrackingProperty属性で装飾されたプロパティを持つクラスがIChangeTrackableインターフェイスを実装しているかどうかを確認することもできます。
この方法は、このスレッドで提供されている一連のさまざまな概念に基づいています。私自身のように、これをきれいにそして効率的に行う方法を探している人のためにそれをそこに出すと思いました。
このハイブリッドコンセプトの鍵は次のとおりです。
これらの要件を考えると、これは私が思いついたものであり、私にとっては完璧に機能しているようであり、UIに対して作業し、ユーザーの変更を正確にキャプチャするときに非常に役立ちます。また、UIでこれをどのように使用するかを示すために、以下の「使用方法」を投稿しました。
オブジェクト
public class MySmartObject
{
public string Name { get; set; }
public int Number { get; set; }
private int clean_hashcode { get; set; }
public bool IsDirty { get { return !(this.clean_hashcode == this.GetHashCode()); } }
public MySmartObject()
{
this.Name = "";
this.Number = -1;
MakeMeClean();
}
public MySmartObject(string name, int number)
{
this.Name = name;
this.Number = number;
MakeMeClean();
}
public void MakeMeClean()
{
this.clean_hashcode = this.Name.GetHashCode() ^ this.Number.GetHashCode();
}
public override int GetHashCode()
{
return this.Name.GetHashCode() ^ this.Number.GetHashCode();
}
}
それは十分に単純であり、私たちのすべての要件に対応しています。
もちろん、これをさまざまな状態に適応させることができます...それは本当にあなた次第です。この例は、適切なIsDirtyフラグ操作を行う方法のみを示しています。
シナリオ
このためのいくつかのシナリオを調べて、何が返ってくるか見てみましょう。
シナリオ1
空のコンストラクターを使用して新しいオブジェクトが作成されます。
プロパティ名が「」から「ジェームズ」に変更されました。
IsDirtyへの呼び出しはTrueを返します!正確。
シナリオ2
新しいオブジェクトは、「John」と12345のパラメータを使用して作成されます。
プロパティ名が「John」から「James」に変更されました。
プロパティ名が「James」から「John」に戻り、
IsDirtyを呼び出すとFalseが返されます。正確であり、データを複製する必要もありませんでした。
使用方法、WinForms UIの例
これは単なる例であり、UIからさまざまな方法でこれを使用できます。
2つの形式([A]と[B])があるとしましょう。
first([A])はメインフォームで、second([B])はユーザーがMySmartObject内の値を変更できるフォームです。
[A]フォームと[B]フォームの両方で、次のプロパティが宣言されています。
public MySmartObject UserKey { get; set; }
ユーザーが[A]フォームのボタンをクリックすると、[B]フォームのインスタンスが作成され、そのプロパティが設定され、ダイアログとして表示されます。
フォーム[B]が戻った後、[A]フォームは[B]フォームのIsDirtyチェックに基づいてプロパティを更新します。このような:
private void btn_Expand_Click(object sender, EventArgs e)
{
SmartForm form = new SmartForm();
form.UserKey = this.UserKey;
if(form.ShowDialog() == DialogResult.OK && form.UserKey.IsDirty)
{
this.UserKey = form.UserKey;
//now that we have saved the "new" version, mark it as clean!
this.UserKey.MakeMeClean();
}
}
また、[B]では、閉じているときに、次のように、ユーザーが未保存の変更を加えてフォームを閉じているかどうかを確認してプロンプトを表示できます。
private void BForm_FormClosing(object sender, FormClosingEventArgs e)
{
//If the user is closing the form via another means than the OK button, or the Cancel button (e.g.: Top-Right-X, Alt+F4, etc).
if (this.DialogResult != DialogResult.OK && this.DialogResult != DialogResult.Ignore)
{
//check if dirty first...
if (this.UserKey.IsDirty)
{
if (MessageBox.Show("You have unsaved changes. Close and lose changes?", "Unsaved Changes", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
e.Cancel = true;
}
}
}
上記の例からわかるように、これはUIを実際に合理化するため、非常に便利なものになる可能性があります。
警告
うまくいけば、これは誰かを助けます。
PostSharp( http://www.postsharp.org/ )を見てください。ダーティとしてマークする属性を簡単に作成できます。属性を必要とする各プロパティに属性を追加すると、すべてのコードが1か所に保持されます。
大まかに言えば、クラスに実装させるためのステータスを持つインターフェースを作成します。マークされたプロパティの1つが変更されたときに値を設定するために、プロパティに適用してインターフェイスにキャストできる属性を作成します。
「タイプを不変にすることを検討してください」というアドバイスとは別に、これが私が書いたものです(そして、JonとMarcに途中で何かを教えてもらいました)
public class Example_Class
{ // snip
// all properties are public get and private set
private Dictionary<string, Delegate> m_PropertySetterMap;
public Example_Class()
{
m_PropertySetterMap = new Dictionary<string, Delegate>();
InitializeSettableProperties();
}
public Example_Class(long id, string name):this()
{ this.ID = id; this.Name = name; }
private void InitializeSettableProperties()
{
AddToPropertyMap<long>("ID", value => { this.ID = value; });
AddToPropertyMap<string>("Name", value => { this.Name = value; });
}
// jump thru a hoop because it won't let me cast an anonymous method to an Action<T>/Delegate
private void AddToPropertyMap<T>(string sPropertyName, Action<T> setterAction)
{ m_PropertySetterMap.Add(sPropertyName, setterAction); }
public void SetProperty<T>(string propertyName, T value)
{
(m_PropertySetterMap[propertyName] as Action<T>).Invoke(value);
this.Status = StatusEnum.Dirty;
}
}
あなたはアイデアを得る..可能な改善:PropertyNamesに定数を使用し、プロパティが実際に変更されたかどうかを確認します。ここでの1つの欠点は、
obj.SetProperty("ID", 700); // will blow up int instead of long
obj.SetProperty<long>("ID", 700); // be explicit or use 700L
あなたのアプローチは基本的に私がそれをする方法です。 Statusプロパティのセッターを削除するだけです。
_public StatusEnum Status
{
get { return _Status; }
// set { _Status = value; }
}
_
代わりに関数を追加します
_public SetStatusClean()
{
_Status = StatusEnum.Clean;
}
_
SetStatusDeleted()
とSetStatusPurged()
と同様に、意図をよりよく示していると思います。
編集
Jon Skeetによる回答 を読んだら、私のアプローチを再考する必要があります;-)単純なオブジェクトの場合、私は自分のやり方に固執しますが、それがより複雑になると、彼の提案ははるかによく整理されたコードにつながります。
Example_Classが軽量の場合は、変更を判別するために、元の状態を保存してから、現在の状態を元の状態と比較することを検討してください。この場合、元の状態をストローすると多くのシステムリソースが消費されるため、そうでない場合は、アプローチが最適です。
これが私がそれをする方法です。
特定のフィールドがダーティであるかどうかをテストする必要がない場合は、抽象クラスがあります。
public abstract class SmartWrap : ISmartWrap
{
private int orig_hashcode { get; set; }
private bool _isInterimDirty;
public bool IsDirty
{
get { return !(this.orig_hashcode == this.GetClassHashCode()); }
set
{
if (value)
this.orig_hashcode = this.orig_hashcode ^ 108.GetHashCode();
else
MakeClean();
}
}
public void MakeClean()
{
this.orig_hashcode = GetClassHashCode();
this._isInterimDirty = false;
}
// must be overridden to return combined hashcodes of fields testing for
// example Field1.GetHashCode() ^ Field2.GetHashCode()
protected abstract int GetClassHashCode();
public bool IsInterimDirty
{
get { return _isInterimDirty; }
}
public void SetIterimDirtyState()
{
_isInterimDirty = this.IsDirty;
}
public void MakeCleanIfInterimClean()
{
if (!IsInterimDirty)
MakeClean();
}
/// <summary>
/// Must be overridden with whatever valid tests are needed to make sure required field values are present.
/// </summary>
public abstract bool IsValid { get; }
}
}
インターフェースだけでなく
public interface ISmartWrap
{
bool IsDirty { get; set; }
void MakeClean();
bool IsInterimDirty { get; }
void SetIterimDirtyState();
void MakeCleanIfInterimClean();
}
これにより、部分的な保存を実行し、保存する他の詳細がある場合はIsDirty状態を保持できます。完璧ではありませんが、多くの分野をカバーしています。
暫定的なIsDirty状態での使用例(わかりやすくするためにエラーの折り返しと検証を削除):
area.SetIterimDirtyState();
if (!UpdateClaimAndStatus(area))
return false;
area.MakeCleanIfInterimClean();
return true;
これはほとんどのシナリオに適していますが、一部のクラスでは、元のデータのバッキングフィールドを使用して各フィールドをテストし、変更のリストを返すか、少なくとも変更されたフィールドの列挙型を返します。フィールドの列挙が変更されたら、メッセージチェーンを介してそれをプッシュアップし、リモートキャッシュ内のフィールドを選択的に更新できます。