การใช้ INotifyPropertyChanged - มีวิธีที่ดีกว่านี้หรือไม่?


647

Microsoft ควรนำสิ่งที่น่าสนใจมาใช้INotifyPropertyChangedเช่นในคุณสมบัติอัตโนมัติเพียงระบุว่า{get; set; notify;} ฉันคิดว่ามันสมเหตุสมผลดี หรือมีภาวะแทรกซ้อนใด ๆ ที่จะทำหรือไม่

ตัวเราเองสามารถใช้บางอย่างเช่น 'แจ้งเตือน' ในคุณสมบัติของเรา มีวิธีการแก้ปัญหาที่สง่างามสำหรับการนำไปใช้INotifyPropertyChangedในชั้นเรียนของคุณหรือวิธีเดียวที่จะทำได้คือการเพิ่มPropertyChangedเหตุการณ์ในแต่ละสถานที่ให้บริการ

หากเราไม่สามารถเขียนบางสิ่งเพื่อสร้างชิ้นส่วนของรหัสโดยอัตโนมัติเพื่อเพิ่มPropertyChanged เหตุการณ์ได้


7
code.google.com/p/notifypropertyweaverอาจใช้งานได้
Ian Ringrose

7
ลิงก์ด้านบนเสียชีวิต github.com/SimonCropp/NotifyPropertyWeaver
23

2
คุณสามารถใช้ DependencyObject และ DependencyProperties แทน ฮา! ฉันทำตลก
Phil


5
ในขณะที่ทำการเปลี่ยนแปลง C # นั้นเป็นไปไม่ได้เนื่องจากเรามีบันทึกด้านหลังขนาดใหญ่ของการพึ่งพาระหว่างกัน เมื่อย้อนกลับไปเมื่อ MVVM เกิดฉันเดาว่าเราแค่ไม่ได้ใช้ความพยายามอย่างมากในการแก้ปัญหานี้และฉันรู้ว่าทีมรูปแบบและการปฏิบัติมีไม่กี่อย่างที่ทำไปตลอดทาง (ดังนั้นคุณยังได้ MEF หัวข้อการวิจัย) วันนี้ฉันคิดว่า [CallerMemberName] เป็นคำตอบสำหรับข้างต้น
Scott Barnes

คำตอบ:


633

เวอร์ชันที่เล็กที่สุดที่ฉันใช้จะไม่ใช้อะไรอย่าง Postsharp:

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

สถานที่ให้บริการแต่ละคนเป็นเพียงสิ่งที่ชอบ:

    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }

ซึ่งไม่ใหญ่มาก มันสามารถใช้เป็นคลาสพื้นฐานได้หากคุณต้องการ boolกลับมาจากการSetFieldบอกคุณว่ามันเป็นไม่มี-op ในกรณีที่คุณต้องการที่จะใช้ตรรกะอื่น ๆ


หรือง่ายยิ่งขึ้นด้วย C # 5:

protected bool SetField<T>(ref T field, T value,
    [CallerMemberName] string propertyName = null)
{...}

ซึ่งสามารถเรียกเช่นนี้:

set { SetField(ref name, value); }

ซึ่งคอมไพเลอร์จะเพิ่ม"Name"โดยอัตโนมัติ


C # 6.0 ทำให้การใช้งานง่ายขึ้น:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

... และตอนนี้ด้วย C # 7:

protected void OnPropertyChanged(string propertyName)
   => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

protected bool SetField<T>(ref T field, T value,[CallerMemberName] string propertyName =  null)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}

private string name;
public string Name
{
    get => name;
    set => SetField(ref name, value);
}

4
นิสัยดีมาร์ค! ผมแนะนำการปรับปรุงใช้นิพจน์แลมบ์ดาแทนชื่อของสถานที่ให้ดูคำตอบของฉัน
โทมัส Levesque

7
@Thomas - แลมบ์ดานั้นดีและดี แต่มันเพิ่มค่าใช้จ่ายมากมายสำหรับบางสิ่งที่ง่ายมาก ๆ เคล็ดลับที่มีประโยชน์ แต่ฉันไม่แน่ใจว่ามันใช้งานได้จริงเสมอ
Marc Gravell

14
@Marc - ใช่มันสามารถปฏิบัติงานอาจทำให้เสื่อมเสีย ... แต่ผมชอบความจริงที่ว่ามันตรวจสอบที่รวบรวมเวลาและ refactored อย่างถูกต้องโดยคำสั่ง "เปลี่ยนชื่อ"
โทมัส Levesque

4
@Gusdor โชคดีที่มี C # 5 ไม่จำเป็นต้องประนีประนอม - คุณสามารถได้รับสิ่งที่ดีที่สุดจากทั้งคู่ผ่าน (ตามบันทึกของ Pedro77)[CallerMemberName]
Marc Gravell

4
@Gusdor ภาษาและกรอบงานแยกออกจากกัน คุณสามารถใช้คอมไพเลอร์ C # 5 เป้าหมาย. NET 4 และเพียงแค่เพิ่มแอตทริบิวต์ที่หายไปด้วยตัวคุณเอง - มันจะทำงานได้ดี มันต้องมีชื่อที่ถูกต้องและอยู่ในเนมสเปซที่ถูกต้อง ไม่จำเป็นต้องอยู่ในชุดประกอบเฉพาะ
Marc Gravell

196

ในฐานะของ. Net 4.5 ในที่สุดก็มีวิธีที่ง่ายในการทำเช่นนี้

.Net 4.5 แนะนำคุณลักษณะข้อมูลผู้โทรเข้าใหม่

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
     // make sure only to call this if the value actually changes

     var handler = PropertyChanged;
     if (handler != null) {
        handler(this, new PropertyChangedEventArgs(caller));
     }
}

อาจเป็นความคิดที่ดีที่จะเพิ่มเครื่องมือเปรียบเทียบลงในฟังก์ชั่นเช่นกัน

EqualityComparer<T>.Default.Equals

ตัวอย่างเพิ่มเติมที่นี่และที่นี่

ดูข้อมูลผู้โทร (C # และ Visual Basic)


12
ยอดเยี่ยม! แต่ทำไมมันถึงเป็นเรื่องธรรมดา?
abatishchev

@abatishchev ฉันเดาว่ามันไม่จำเป็นต้องเป็นฉันเพิ่งเล่นกับความคิดของการมีฟังก์ชั่นการตั้งค่าคุณสมบัติเช่นกัน ฉันจะดูว่าฉันสามารถอัปเดตคำตอบของฉันให้โซลูชั่นที่สมบูรณ์ ตัวอย่างเพิ่มเติมทำผลงานได้ดีในระหว่างนี้
Daniel Little

3
มันถูกนำมาใช้โดย C # 5.0 มันไม่เกี่ยวอะไรกับ. net 4.5 แต่นี่เป็นทางออกที่ยอดเยี่ยม!
J. Lennon

5
@J Lennon .net 4.5 ยังมีบางสิ่งที่เกี่ยวข้องกับมันหลังจากที่แอตทริบิวต์ทั้งหมดมาจากที่ใดที่หนึ่งmsdn.microsoft.com/en-au/library/ …
แดเนียล Little

@Lavinski เปลี่ยนใบสมัครของคุณเป็นเช่น. NET 3.5 และดูว่าจะทำงานอย่างไร (ใน vs2012)
J. Lennon

162

ฉันชอบโซลูชันของ Marc แต่ฉันคิดว่ามันสามารถปรับปรุงได้เล็กน้อยเพื่อหลีกเลี่ยงการใช้ "magic string" (ซึ่งไม่รองรับการปรับโครงสร้างใหม่) แทนที่จะใช้ชื่อคุณสมบัติเป็นสตริงคุณสามารถทำให้เป็นแลมบ์ดาได้อย่างง่ายดาย:

private string name;
public string Name
{
    get { return name; }
    set { SetField(ref name, value, () => Name); }
}

เพียงเพิ่มวิธีการต่อไปนี้ในรหัสของ Marc มันจะทำการหลอกลวง:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    if (selectorExpression == null)
        throw new ArgumentNullException("selectorExpression");
    MemberExpression body = selectorExpression.Body as MemberExpression;
    if (body == null)
        throw new ArgumentException("The body must be a member expression");
    OnPropertyChanged(body.Member.Name);
}

protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(selectorExpression);
    return true;
}

BTW สิ่งนี้ได้รับแรงบันดาลใจจากURL ที่อัปเดตเมื่อโพสต์บล็อก


6
มีกรอบการทำงานอย่างน้อยหนึ่งใช้วิธีนี้ReactiveUI
AlSki

ดึกมากนี่หมายถึงต้องผ่านการไตร่ตรอง อาจเป็นที่ยอมรับได้ แต่การตั้งค่าคุณสมบัติไม่ใช่สถานที่ที่ฉันต้องการให้ใบสมัครของฉันใช้จ่ายไปหลายรอบ
Bruno Brant

1
@BrunoBrant คุณแน่ใจหรือไม่ว่ามีการแสดงที่มีประสิทธิภาพ ตามการโพสต์บล็อกการสะท้อนเกิดขึ้นในช่วงเวลารวบรวมมากกว่ารันไทม์ (เช่นการสะท้อนคงที่)
นาธาเนียลเอลกินส์

6
ฉันเชื่อว่า OnPropertyChanged ทั้งหมดของคุณ <T> ล้าสมัยกับผู้ให้บริการชื่อ C # 6 ทำให้มอนสเตอร์ตัวนี้ดูโฉบเฉี่ยว
Traubenfuchs

5
@ Traubenfuchs ที่จริงแล้วแอตทริบิวต์ CallerMemberName ของ C # 5 ทำให้มันง่ายยิ่งขึ้นเนื่องจากคุณไม่จำเป็นต้องผ่านสิ่งใดเลย ...
Thomas Levesque

120

นอกจากนี้ยังมีFodyที่มีคุณสมบัติการเปลี่ยนแปลงของคุณสมบัติซึ่งช่วยให้คุณสามารถเขียนสิ่งนี้:

[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
}

... และเมื่อถึงเวลารวบรวมคุณสมบัติการแจ้งเตือนก็เปลี่ยนไป


7
ผมคิดว่านี่เป็นสิ่งที่ OP กำลังมองหาเมื่อพวกเขาถามว่า "เราสามารถตัวเองดำเนินการบางสิ่งบางอย่างเช่น 'แจ้ง' ในคุณสมบัติของเรามีวิธีการแก้ปัญหาที่สง่างามในการดำเนิน INotifyPropertyChanged ในชั้นเรียนของคุณ."
Ashoat

3
นี่เป็นทางออกที่งดงามเพียงอย่างเดียวและทำงานได้อย่างไร้ที่ติ @CADbloke กล่าว และฉันก็สงสัยเกี่ยวกับช่างทอผ้าด้วยเช่นกัน แต่ฉันตรวจสอบ / ตรวจสอบรหัส IL ด้านหลังอีกครั้งและมันสมบูรณ์แบบเรียบง่ายทำในสิ่งที่คุณต้องการและไม่มีใครอื่น นอกจากนี้ยังขอและเรียกชื่อวิธีการใด ๆ ที่คุณได้กำหนดไว้ในคลาสพื้นฐานไม่ว่า NotifyOnProp ... , OnNotify ... ไม่สำคัญดังนั้นทำงานได้ดีกับคลาสพื้นฐานใด ๆ ที่คุณอาจมีและใช้ INOTify .. .
NSGaga ส่วนใหญ่ไม่ได้ใช้งาน

1
คุณสามารถตรวจสอบสิ่งที่ช่างทอผ้าทำซ้ำได้อย่างง่ายดายดูที่หน้าต่างผลลัพธ์ของบิลด์มันแสดงรายการคุณสมบัติทั้งหมดที่มีการทอ การใช้ส่วนขยาย VScolorOutput กับรูปแบบ regex "Fody/.*?:",LogCustom2,Trueจะเน้นสีใน "Custom 2" ฉันทำให้สีชมพูสดใสดังนั้นจึงหาง่าย เพียงทำทุกอย่างเป็นวิธีที่ประณีตที่สุดในการทำสิ่งใดก็ตามที่มีการพิมพ์ซ้ำ ๆ มากมาย
CAD bloke

@mahmoudnezarsarhan ไม่ไม่ใช่ฉันจำได้ว่ามีการเปลี่ยนแปลงเล็กน้อยในวิธีการกำหนดค่า แต่Fody PropertyChangedยังคงมีชีวิตและใช้งานอยู่
Larry

65

ฉันคิดว่าผู้คนควรให้ความสนใจกับการแสดงเพิ่มขึ้นเล็กน้อย มันจะส่งผลกระทบต่อ UI เมื่อมีวัตถุจำนวนมากที่ถูกผูกไว้ (คิดว่าเป็นกริดที่มี 10,000+ แถว) หรือหากค่าของวัตถุเปลี่ยนแปลงบ่อยครั้ง (แอพตรวจสอบแบบเรียลไทม์)

ฉันใช้การดำเนินการต่างๆที่นี่และที่อื่น ๆ และทำการเปรียบเทียบ; ตรวจสอบออกperfomance เปรียบเทียบของการใช้งาน


นี่คือผลที่ตามมา Implemenation vs Runtime


14
-1: ไม่มีค่าใช้จ่ายด้านประสิทธิภาพ: CallerMemberName จะถูกเปลี่ยนเป็นค่าตามตัวอักษรในเวลารวบรวม เพียงลองและถอดรหัสแอปของคุณ
JYL

นี่คือคำถามและคำตอบตาม: stackoverflow.com/questions/22580623/…
uli78

1
@JYL คุณถูกต้องแล้วที่ CallerMemberName ไม่ได้เพิ่มโอเวอร์เฮดขนาดใหญ่ ฉันต้องดำเนินการบางอย่างผิดพลาดครั้งล่าสุดที่ฉันลอง ฉันจะอัปเดตบล็อกและคำตอบเพื่อสะท้อนถึงเกณฑ์มาตรฐานสำหรับการใช้งาน CallerMemberName และ Fody ในภายหลัง
Peijen

1
หากคุณมีกริด 10,000+ ใน UI คุณควรรวมวิธีต่างๆเพื่อจัดการกับประสิทธิภาพเช่นเพจที่คุณแสดง 10, 50, 100, 250 ครั้งต่อหน้าเท่านั้น ...
Austin Rhymer

Austin Rhymer หากคุณมี data ขนาดใหญ่ + 50 virtualisation ใช้ data ไม่จำเป็นต้องโหลดข้อมูลทั้งหมดมันจะทำการโหลดเฉพาะข้อมูลที่มองเห็นได้ในพื้นที่แสดงภาพที่กำลังหมุนตัวในปัจจุบัน!
Bilal

38

ฉันแนะนำคลาส Bindable ในบล็อกของฉันที่http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable ใช้พจนานุกรมเป็นกระเป๋าคุณสมบัติ มันง่ายพอที่จะเพิ่มโอเวอร์โหลดที่จำเป็นสำหรับคลาสย่อยเพื่อจัดการฟิลด์สำรองของตัวเองโดยใช้พารามิเตอร์อ้างอิง

  • ไม่มีสายเวท
  • ไม่มีการสะท้อนกลับ
  • สามารถปรับปรุงเพื่อระงับการค้นหาพจนานุกรมเริ่มต้น

รหัส:

public class Bindable : INotifyPropertyChanged {
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    /// <summary>
    /// Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    protected T Get<T>([CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T)value;
        return default(T);
    }

    /// <summary>
    /// Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    /// <remarks>Use this overload when implicitly naming the property</remarks>
    protected void Set<T>(T value, [CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        if (Equals(value, Get<T>(name)))
            return;
        _properties[name] = value;
        OnPropertyChanged(name);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

มันสามารถใช้เช่นนี้:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}

2
นี่เป็นทางออกที่ดี แต่ข้อเสียอย่างเดียวคือมีการแสดงที่เกี่ยวข้องกับการชกมวย / การไม่ทำกล่อง
MCattle

1
ฉันขอแนะนำให้ใช้protected T Get<T>(T defaultValue, [CallerMemberName] string name = null)และตรวจสอบif (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))อยู่ในชุด (ที่จะยกระดับและบันทึกชุดตอนแรกที่เป็นค่าเริ่มต้น)
Miquel

1
@Miquel เพิ่มการสนับสนุนสำหรับค่าเริ่มต้นที่กำหนดเองจะมีประโยชน์แน่นอน แต่คุณควรระมัดระวังที่จะเพิ่มเหตุการณ์ที่เปลี่ยนแปลงเท่านั้นเมื่อค่าที่เปลี่ยนแปลงจริง การตั้งค่าคุณสมบัติเป็นค่าเดียวกันที่มีไม่ควรเพิ่มเหตุการณ์ ฉันต้องยอมรับในกรณีส่วนใหญ่มันไม่เป็นอันตราย แต่ฉันได้รับค่อนข้างสองสามครั้งด้วยคุณสมบัติที่ตั้งเวลานับพันเป็นค่าเดียวกันกับเหตุการณ์ที่ทำลายการตอบสนองของ UI
TiMoch

1
@stakx ฉันมีแอปพลิเคชั่นไม่กี่ตัวที่สร้างบนนี้เพื่อรองรับรูปแบบของที่ระลึกสำหรับการเลิกทำ / ทำซ้ำหรือเพื่อเปิดใช้งานรูปแบบการทำงานในแอปพลิเคชันที่ nhibernate ไม่สามารถใช้งานได้
TiMoch

1
ฉันชอบโซลูชันนี้โดยเฉพาะ: สัญกรณ์สั้น ๆ ไม่มีพร็อกซีแบบไดนามิกไม่มีการแทรกแซงของ IL ฯลฯ แม้ว่าคุณจะสามารถทำให้สั้นลงได้โดยไม่จำเป็นต้องระบุ T ทุกครั้งที่รับโดยทำให้ Get return แบบไดนามิก ฉันรู้ว่าสิ่งนี้ส่งผลกระทบต่อประสิทธิภาพรันไทม์ แต่ตอนนี้รหัสสำหรับผู้ได้รับและผู้ตั้งได้ในที่สุดก็สามารถเหมือนกันและในบรรทัดเดียวสรรเสริญพระเจ้า! ป.ล. คุณควรใช้ความระมัดระวังเพิ่มเติมในวิธีการรับของคุณ (หนึ่งครั้งเมื่อคุณเขียนคลาสฐาน) เมื่อส่งกลับค่าเริ่มต้นสำหรับประเภทที่มีค่าเป็นแบบไดนามิก อย่าลืมส่งคืนค่าเริ่มต้นที่ถูกต้องเสมอ (สามารถทำได้)
evilkos

15

ฉันยังไม่ได้มีโอกาสลองทำเอง แต่ครั้งต่อไปที่ฉันตั้งค่าโครงการด้วยข้อกำหนดที่ยิ่งใหญ่สำหรับ INotifyPropertyChanged ฉันตั้งใจจะเขียนแอตทริบิวต์Postsharpซึ่งจะฉีดโค้ดในเวลารวบรวม สิ่งที่ต้องการ:

[NotifiesChange]
public string FirstName { get; set; }

จะกลายเป็น:

private string _firstName;

public string FirstName
{
   get { return _firstname; }
   set
   {
      if (_firstname != value)
      {
          _firstname = value;
          OnPropertyChanged("FirstName")
      }
   }
}

ฉันไม่แน่ใจว่าสิ่งนี้จะได้ผลในทางปฏิบัติหรือไม่และฉันต้องนั่งลงและลองทำ แต่ฉันไม่เห็นว่าทำไม ฉันอาจต้องทำให้ยอมรับพารามิเตอร์บางอย่างสำหรับสถานการณ์ที่จำเป็นต้องเปิดใช้งานมากกว่าหนึ่ง OnPropertyChanged (ตัวอย่างเช่นถ้าฉันมีคุณสมบัติ FullName ในคลาสด้านบน)

ขณะนี้ฉันกำลังใช้เทมเพลตที่กำหนดเองใน Resharper แต่ถึงแม้ว่าฉันจะเบื่อกับคุณสมบัติทั้งหมดของฉันเป็นเวลานาน


อารวดเร็วการค้นหาของ Google (ซึ่งผมควรจะทำก่อนที่ผมเขียนนี้) แสดงให้เห็นว่าอย่างน้อยหนึ่งคนมีสิ่งที่ทำเช่นนี้มาก่อนที่นี่ ไม่ใช่สิ่งที่ฉันมีในใจ แต่ใกล้พอที่จะแสดงให้เห็นว่าทฤษฎีนั้นดี


6
เครื่องมือฟรีที่ชื่อว่า Fody ดูเหมือนจะทำสิ่งเดียวกันโดยทำหน้าที่เป็นหัวฉีดโค้ดคอมไพล์เวลาทั่วไป มันสามารถดาวน์โหลดได้ใน Nuget เช่นเดียวกับแพ็คเกจปลั๊กอิน PropertyChanged และ PropertyChanging
Triynko

11

ใช่มีวิธีที่ดีกว่าอย่างแน่นอน นี่มันคือ:

การสอนทีละขั้นตอนหดตัวลงโดยฉันตามสิ่งนี้ บทความที่มีประโยชน์

  • สร้างโครงการใหม่
  • ติดตั้งแพ็คเกจหลักของ Castle ในโครงการ

ติดตั้งแพ็คเกจปราสาท

  • ติดตั้งไลบรารี่ mvvm เท่านั้น

ติดตั้งแพคเกจ MvvmLightLibs

  • เพิ่มสองคลาสในโครงการ:

NotifierInterceptor

public class NotifierInterceptor : IInterceptor
    {
        private PropertyChangedEventHandler handler;
        public static Dictionary<String, PropertyChangedEventArgs> _cache =
          new Dictionary<string, PropertyChangedEventArgs>();

        public void Intercept(IInvocation invocation)
        {
            switch (invocation.Method.Name)
            {
                case "add_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                case "remove_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                default:
                    if (invocation.Method.Name.StartsWith("set_"))
                    {
                        invocation.Proceed();
                        if (handler != null)
                        {
                            var arg = retrievePropertyChangedArg(invocation.Method.Name);
                            handler(invocation.Proxy, arg);
                        }
                    }
                    else invocation.Proceed();
                    break;
            }
        }

        private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
        {
            PropertyChangedEventArgs arg = null;
            _cache.TryGetValue(methodName, out arg);
            if (arg == null)
            {
                arg = new PropertyChangedEventArgs(methodName.Substring(4));
                _cache.Add(methodName, arg);
            }
            return arg;
        }
    }

ProxyCreator

public class ProxyCreator
{
    public static T MakeINotifyPropertyChanged<T>() where T : class, new()
    {
        var proxyGen = new ProxyGenerator();
        var proxy = proxyGen.CreateClassProxy(
          typeof(T),
          new[] { typeof(INotifyPropertyChanged) },
          ProxyGenerationOptions.Default,
          new NotifierInterceptor()
          );
        return proxy as T;
    }
}
  • สร้างโมเดลมุมมองของคุณตัวอย่างเช่น:

-

 public class MainViewModel
    {
        public virtual string MainTextBox { get; set; }

        public RelayCommand TestActionCommand
        {
            get { return new RelayCommand(TestAction); }
        }

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • ใส่การผูกไว้ใน xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
  • ใส่บรรทัดของโค้ดในไฟล์โค้ด - หลัง MainWindow.xaml.cs ดังนี้:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • สนุก.

ป้อนคำอธิบายรูปภาพที่นี่

เรียน !!! คุณสมบัติที่ล้อมรอบทั้งหมดควรได้รับการตกแต่งด้วยคำสำคัญเสมือนจริงเพราะคุณสมบัติเหล่านี้ใช้โดยพร็อกซีปราสาทเพื่อการแทนที่


ฉันสนใจที่จะทราบว่าคุณกำลังใช้ Castle รุ่นใด ผมใช้ 3.3.0 และวิธี CreateClassProxy typeไม่ได้มีพารามิเตอร์เหล่านั้น: interfaces to apply, interceptors,
IAbstract

ไม่เป็นไรฉันใช้CreateClassProxy<T>วิธีการทั่วไป แตกต่างกันมาก ... hmmm สงสัยว่าทำไมจึง จำกัด ด้วยวิธีการทั่วไป :(
IAbstract

7

วิธีที่คล้ายกับ AOP มากคือการฉีดข้อมูล INotifyPropertyChanged ลงบนวัตถุที่สร้างอินสแตนซ์ได้ทันที คุณสามารถทำสิ่งนี้กับ Castle DynamicProxy นี่คือบทความที่อธิบายเทคนิค:

การเพิ่ม INotifyPropertyChanged ให้กับวัตถุที่มีอยู่


5

ดูนี่ : http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

มันเขียนเป็นภาษาเยอรมัน แต่คุณสามารถดาวน์โหลด ViewModelBase.cs ความคิดเห็นทั้งหมดใน cs-File เขียนเป็นภาษาอังกฤษ

ด้วย ViewModelBase-Class นี้มันเป็นไปได้ที่จะใช้คุณสมบัติ bindable ที่คล้ายกับคุณสมบัติ Dependency ที่รู้จักกันดี:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}

1
ลิงก์เสีย
Guge

4

จากคำตอบของ Thomas ซึ่งดัดแปลงมาจากคำตอบของ Marc ฉันได้เปลี่ยนคุณสมบัติการสะท้อนการเปลี่ยนรหัสเป็นคลาสพื้นฐาน:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) 
            handler(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null)
            throw new ArgumentNullException("selectorExpression");
        var me = selectorExpression.Body as MemberExpression;

        // Nullable properties can be nested inside of a convert function
        if (me == null)
        {
            var ue = selectorExpression.Body as UnaryExpression;
            if (ue != null)
                me = ue.Operand as MemberExpression;
        }

        if (me == null)
            throw new ArgumentException("The body must be a member expression");

        OnPropertyChanged(me.Member.Name);
    }

    protected void SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression, params Expression<Func<object>>[] additonal)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return;
        field = value;
        OnPropertyChanged(selectorExpression);
        foreach (var item in additonal)
            OnPropertyChanged(item);
    }
}

การใช้งานเหมือนกับคำตอบของ Thomas ยกเว้นว่าคุณสามารถส่งคุณสมบัติเพิ่มเติมเพื่อแจ้งให้ทราบ นี่เป็นสิ่งจำเป็นในการจัดการคอลัมน์จากการคำนวณซึ่งจำเป็นต้องรีเฟรชในตาราง

private int _quantity;
private int _price;

public int Quantity 
{ 
    get { return _quantity; } 
    set { SetField(ref _quantity, value, () => Quantity, () => Total); } 
}
public int Price 
{ 
    get { return _price; } 
    set { SetField(ref _price, value, () => Price, () => Total); } 
}
public int Total { get { return _price * _quantity; } }

ฉันมีสิ่งนี้ขับคอลเลกชันของรายการที่เก็บไว้ใน BindingList เปิดเผยผ่าน DataGridView มันไม่จำเป็นที่ฉันจะต้องทำการโทรด้วยตนเอง Refresh () ไปยังกริด


4

ผมขอแนะนำวิธีการของตัวเองที่เรียกว่าYappi Yappiมันเป็นของ Runtime proxy | ตัวสร้างคลาสที่ได้รับเพิ่มการทำงานใหม่ให้กับวัตถุหรือชนิดที่มีอยู่เช่น Dynamic Proxy ของ Caste Project

อนุญาตให้ใช้งาน INotifyPropertyChanged หนึ่งครั้งในคลาสพื้นฐานแล้วประกาศคลาสที่ได้รับในรูปแบบต่อไปนี้ยังคงสนับสนุน INotifyPropertyChanged สำหรับคุณสมบัติใหม่:

public class Animal:Concept
{
    protected Animal(){}
    public virtual string Name { get; set; }
    public virtual int Age { get; set; }
}

ความซับซ้อนของคลาสที่ได้รับหรือการสร้างพร็อกซีสามารถซ่อนอยู่หลังบรรทัดต่อไปนี้:

var animal = Concept.Create<Animal>.New();

และงานติดตั้ง INotifyPropertyChanged ทั้งหมดสามารถทำได้ดังนี้:

public class Concept:INotifyPropertyChanged
{
    //Hide constructor
    protected Concept(){}

    public static class Create<TConcept> where TConcept:Concept
    {
        //Construct derived Type calling PropertyProxy.ConstructType
        public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
        //Create constructing delegate calling Constructor.Compile
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
    {
        var caller = PropertyChanged;
        if(caller!=null)
        {
            caller(this, eventArgs);
        }
    }

    //define implementation
    public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
    {
        public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
        }
        /// <summary>
        /// Overriding property setter implementation.
        /// </summary>
        /// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
        /// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
        /// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
        /// <typeparam name="TResult">Type of property.</typeparam>
        /// <param name="property">PropertyInfo of property.</param>
        /// <returns>Delegate, corresponding to property setter implementation.</returns>
        public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            //This code called once for each declared property on derived type's initialization.
            //EventArgs instance is shared between all events for each concrete property.
            var eventArgs = new PropertyChangedEventArgs(property.Name);
            //get delegates for base calls.
            Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
            Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);

            var comparer = EqualityComparer<TResult>.Default;

            return (pthis, value) =>
            {//This code executes each time property setter is called.
                if (comparer.Equals(value, getter(pthis))) return;
                //base. call
                setter(pthis, value);
                //Directly accessing Concept's protected method.
                pthis.OnPropertyChanged(eventArgs);
            };
        }
    }
}

ปลอดภัยอย่างยิ่งสำหรับการปรับโครงสร้างใหม่ไม่ต้องใช้การสะท้อนหลังจากการก่อสร้างประเภทและเร็วพอ


ทำไมคุณต้องTDeclarationพิมพ์พารามิเตอร์ในPropertyImplementation? แน่นอนคุณสามารถค้นหาประเภทที่เหมาะสมในการโทร (ไม่ใช่ callvirt) จาก getter / setter จากด้วยเท่านั้นTImplementation?
Andrew Savinykh

การดำเนินการที่ใช้ในกรณีส่วนใหญ่ ข้อยกเว้นคือ: 1. คุณสมบัตินิยามใหม่ด้วย "new" C # keyvord 2. คุณสมบัติของการใช้อินเทอร์เฟซที่ชัดเจน
Kelqualyn

3

คำตอบทั้งหมดนี้ดีมาก

โซลูชันของฉันใช้ตัวอย่างโค้ดเพื่อทำงาน

สิ่งนี้ใช้การเรียกที่ง่ายที่สุดไปยังเหตุการณ์ PropertyChanged

บันทึกตัวอย่างนี้และใช้ในขณะที่คุณใช้ตัวอย่าง 'fullprop'

ดูตำแหน่งได้ที่เมนู 'เครื่องมือ \ โค้ดเครื่องมือจัดการโค้ด ... ' ที่ Visual Studio

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>inotifypropfull</Title>
            <Shortcut>inotifypropfull</Shortcut>
            <HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
            <Description>Code snippet for property and backing field with notification</Description>
            <Author>Ofir Zeitoun</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
                <![CDATA[private $type$ $field$;

    public $type$ $property$
    {
        get { return $field$;}
        set { 
            $field$ = value;
            var temp = PropertyChanged;
            if (temp != null)
            {
                temp(this, new PropertyChangedEventArgs("$property$"));
            }
        }
    }
    $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

คุณสามารถแก้ไขการโทรได้ตามต้องการ (เพื่อใช้วิธีแก้ไขปัญหาข้างต้น)


2

ถ้าคุณกำลังใช้การเปลี่ยนแปลงใน .NET 4.5 INotifyPropertyChangedคุณไม่จำเป็นต้องกังวลเกี่ยวกับ

dynamic obj = new ExpandoObject();
obj.Name = "John";

ถ้าชื่อถูกผูกไว้กับการควบคุมบางอย่างมันก็ใช้ได้ดี


1
ข้อเสียของการใช้สิ่งนี้?
juFo

2

โซลูชันรวมอื่นกำลังใช้ StackFrame:

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void Set<T>(ref T field, T value)
    {
        MethodBase method = new StackFrame(1).GetMethod();
        field = value;
        Raise(method.Name.Substring(4));
    }

    protected void Raise(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
        {
            temp(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

การใช้งาน:

public class TempVM : BaseViewModel
{
    private int _intP;
    public int IntP
    {
        get { return _intP; }
        set { Set<int>(ref _intP, value); }
    }
}

2
เร็วไหม ไม่สามารถเข้าถึงกรอบสแต็กที่ผูกไว้กับข้อกำหนดการอนุญาตบางอย่างได้หรือไม่ มันแข็งแกร่งในบริบทของการใช้ async / คอยหรือไม่?
Stéphane Gourichon

@ StéphaneGourichonไม่มันไม่ใช่ การเข้าถึงกรอบสแต็กหมายถึงประสิทธิภาพที่ดีเยี่ยมในกรณีส่วนใหญ่
Bruno Brant

ใช่มีคุณสามารถดูได้ที่codereview.stackexchange.com/questions/13823/…
Ofir

โปรดทราบว่าการฝังไว้อาจซ่อนget_Fooวิธีการในโหมด Release
bytecode77

2

ฉันสร้างวิธีการขยายในไลบรารีฐานของฉันเพื่อนำมาใช้ใหม่:

public static class INotifyPropertyChangedExtensions
{
    public static bool SetPropertyAndNotify<T>(this INotifyPropertyChanged sender,
               PropertyChangedEventHandler handler, ref T field, T value, 
               [CallerMemberName] string propertyName = "",
               EqualityComparer<T> equalityComparer = null)
    {
        bool rtn = false;
        var eqComp = equalityComparer ?? EqualityComparer<T>.Default;
        if (!eqComp.Equals(field,value))
        {
            field = value;
            rtn = true;
            if (handler != null)
            {
                var args = new PropertyChangedEventArgs(propertyName);
                handler(sender, args);
            }
        }
        return rtn;
    }
}

นี้ทำงานร่วมกับสุทธิ 4.5 เนื่องจากการCallerMemberNameAttribute หากคุณต้องการใช้กับรุ่น. Net รุ่นก่อนหน้าคุณต้องเปลี่ยนวิธีการประกาศจาก: ...,[CallerMemberName] string propertyName = "", ...เป็น...,string propertyName, ...

การใช้งาน:

public class Dog : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
        }
    }
}

2

ฉันแก้ไขด้วยวิธีนี้ (มันเป็นห้องทดลองเล็กน้อย แต่แน่นอนว่าเร็วกว่าในรันไทม์)

ใน VB (ขออภัย แต่ฉันคิดว่ามันแปลไม่ยากใน C #) ฉันทำการทดแทนด้วย RE:

(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)( |\r\n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |\r\n](?![ |\r\n]*Get)

ด้วย:

Private _${Name} As ${Type}\r\n${Attr}\r\n${Def}${Name} As ${Type}\r\nGet\r\nReturn _${Name}\r\nEnd Get\r\nSet (Value As ${Type})\r\nIf _${Name} <> Value Then \r\n_${Name} = Value\r\nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))\r\nEnd If\r\nEnd Set\r\nEnd Property\r\n

transofrm รหัสทั้งหมดเช่นนี้:

<Bindable(True)>
Protected Friend Property StartDate As DateTime?

ใน

Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
    Get
        Return _StartDate
    End Get
    Set(Value As DateTime?)
        If _StartDate <> Value Then
            _StartDate = Value
            RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
        End If
    End Set
End Property

และถ้าฉันต้องการมีโค้ดที่อ่านได้มากขึ้นฉันสามารถเป็นตรงกันข้ามได้โดยทำการแทนค่าต่อไปนี้:

Private _(?<Name>.*) As (?<Type>.*)[\r\n ]*(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)[\r\n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )\k<Name> As \k<Type>[\r\n ]*Get[\r\n ]*Return _\k<Name>[\r\n ]*End Get[\r\n ]*Set\(Value As \k<Type>\)[\r\n ]*If _\k<Name> <> Value Then[\r\n ]*_\k<Name> = Value[\r\n ]*RaiseEvent PropertyChanged\(Me, New (.*ComponentModel\.)PropertyChangedEventArgs\("\k<Name>"\)\)[\r\n ]*End If[\r\n ]*End Set[\r\n ]*End Property

กับ

${Attr} ${Def} ${Name} As ${Type}

ฉันโยนเพื่อแทนที่รหัส IL ของวิธีการตั้งค่า แต่ฉันไม่สามารถเขียนโค้ดที่คอมไพล์ได้จำนวนมากใน IL ... ถ้าวันหนึ่งฉันเขียนมันฉันจะบอกคุณ!


2

ฉันเก็บเรื่องนี้ไว้เป็นตัวอย่าง C # 6 เพิ่มไวยากรณ์ที่ดีสำหรับการเรียกใช้ตัวจัดการ

// INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

private void Set<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(property, value) == false)
    {
        property = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

2

นี่คือ Unity3D หรือ Non-CallerMemberName รุ่น NotifyPropertyChanged

public abstract class Bindable : MonoBehaviour, INotifyPropertyChanged
{
    private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
    private static readonly StackTrace stackTrace = new StackTrace();
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    ///     Resolves a Property's name from a Lambda Expression passed in.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="property"></param>
    /// <returns></returns>
    internal string GetPropertyName<T>(Expression<Func<T>> property)
    {
        var expression = (MemberExpression) property.Body;
        var propertyName = expression.Member.Name;

        Debug.AssertFormat(propertyName != null, "Bindable Property shouldn't be null!");
        return propertyName;
    }

    #region Notification Handlers

    /// <summary>
    ///     Notify's all other objects listening that a value has changed for nominated propertyName
    /// </summary>
    /// <param name="propertyName"></param>
    internal void NotifyOfPropertyChange(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    ///     Notifies subscribers of the property change.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="property">The property expression.</param>
    internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
    {
        var propertyName = GetPropertyName(property);
        NotifyOfPropertyChange(propertyName);
    }

    /// <summary>
    ///     Raises the <see cref="PropertyChanged" /> event directly.
    /// </summary>
    /// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
    internal void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    #endregion

    #region Getters

    /// <summary>
    ///     Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    internal T Get<T>(Expression<Func<T>> property)
    {
        var propertyName = GetPropertyName(property);
        return Get<T>(GetPropertyName(property));
    }

    /// <summary>
    ///     Gets the value of a property automatically based on its caller.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    internal T Get<T>()
    {
        var name = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
        return Get<T>(name);
    }

    /// <summary>
    ///     Gets the name of a property based on a string.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    internal T Get<T>(string name)
    {
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T) value;
        return default(T);
    }

    #endregion

    #region Setters

    /// <summary>
    ///     Sets the value of a property whilst automatically looking up its caller name.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    internal void Set<T>(T value)
    {
        var propertyName = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
        Set(value, propertyName);
    }

    /// <summary>
    ///     Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    internal void Set<T>(T value, string propertyName)
    {
        Debug.Assert(propertyName != null, "name != null");
        if (Equals(value, Get<T>(propertyName)))
            return;
        _properties[propertyName] = value;
        NotifyOfPropertyChange(propertyName);
    }

    /// <summary>
    ///     Sets the value of a property based off an Expression (()=>FieldName)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="property"></param>
    internal void Set<T>(T value, Expression<Func<T>> property)
    {
        var propertyName = GetPropertyName(property);

        Debug.Assert(propertyName != null, "name != null");

        if (Equals(value, Get<T>(propertyName)))
            return;
        _properties[propertyName] = value;
        NotifyOfPropertyChange(propertyName);
    }

    #endregion
}

รหัสนี้ช่วยให้คุณสามารถเขียนเขตข้อมูลสำรองคุณสมบัติเช่นนี้:

  public string Text
    {
        get { return Get<string>(); }
        set { Set(value); }
    }

นอกจากนี้ใน resharper ถ้าคุณสร้างแพทเทิร์น / ตัวอย่างการค้นหาคุณสามารถทำให้เวิร์กโฟลว์ของคุณเป็นแบบอัตโนมัติได้ด้วยการแปลงฟิลด์ prop แบบง่าย ๆ ไปเป็นข้อมูลสำรองข้างต้น

รูปแบบการค้นหา:

public $type$ $fname$ { get; set; }

แทนที่รูปแบบ:

public $type$ $fname$
{
    get { return Get<$type$>(); }
    set { Set(value); }
}

2

ฉันได้เขียนบทความที่ช่วยในเรื่องนี้ ( https://msdn.microsoft.com/magazine/mt736453 ) คุณสามารถใช้แพ็คเกจ SolSoft.DataBinding NuGet จากนั้นคุณสามารถเขียนโค้ดดังนี้:

public class TestViewModel : IRaisePropertyChanged
{
  public TestViewModel()
  {
    this.m_nameProperty = new NotifyProperty<string>(this, nameof(Name), null);
  }

  private readonly NotifyProperty<string> m_nameProperty;
  public string Name
  {
    get
    {
      return m_nameProperty.Value;
    }
    set
    {
      m_nameProperty.SetValue(value);
    }
  }

  // Plus implement IRaisePropertyChanged (or extend BaseViewModel)
}

ประโยชน์ที่ได้รับ:

  1. ชั้นฐานเป็นตัวเลือก
  2. ไม่มีการสะท้อนในทุกค่าที่ตั้งไว้
  3. สามารถมีคุณสมบัติที่ขึ้นอยู่กับคุณสมบัติอื่น ๆ และพวกเขาทั้งหมดจะเพิ่มเหตุการณ์ที่เหมาะสมโดยอัตโนมัติ (บทความมีตัวอย่างของสิ่งนี้)

2

แม้ว่าจะมีวิธีการมากมายที่ชัดเจนในการทำเช่นนี้ยกเว้นคำตอบเวทมนต์ของ AOP ดูเหมือนว่าไม่มีคำตอบใด ๆ ที่จะมองไปที่การตั้งค่าคุณสมบัติของ Model โดยตรงจากโมเดลมุมมองโดยไม่มีฟิลด์ท้องถิ่นเพื่ออ้างอิง

ปัญหาคือคุณไม่สามารถอ้างอิงคุณสมบัติ อย่างไรก็ตามคุณสามารถใช้การกระทำเพื่อตั้งค่าคุณสมบัตินั้น

protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
    {
        return false;
    }

    property(newValue);
    RaisePropertyChanged(propertyName);
    return true;
}

สามารถใช้เช่นเดียวกับการแยกรหัสต่อไปนี้

public int Prop {
    get => model.Prop;
    set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}

ลองดูสิ BitBucket repoเพื่อดูวิธีการใช้งานอย่างเต็มรูปแบบและวิธีการที่แตกต่างกันสองสามวิธีเพื่อให้ได้ผลลัพธ์เดียวกันซึ่งรวมถึงวิธีที่ใช้ LINQ และวิธีการที่ใช้การสะท้อนกลับ โปรดทราบว่าวิธีการเหล่านี้มีประสิทธิภาพช้าลงอย่างชาญฉลาด


1

สิ่งอื่น ๆ ที่คุณอาจต้องการพิจารณาเมื่อใช้คุณสมบัติเหล่านี้คือข้อเท็จจริงที่ว่า INotifyPropertyChang * ed * ing ทั้งคู่ใช้คลาสอาร์กิวเมนต์ของเหตุการณ์

หากคุณมีคุณสมบัติจำนวนมากที่ตั้งค่าไว้จำนวนอินสแตนซ์ของคลาสอาร์กิวเมนต์ของเหตุการณ์อาจมีขนาดใหญ่มากคุณควรพิจารณาแคชเนื่องจากเป็นหนึ่งในพื้นที่ที่อาจเกิดการระเบิดของสตริงได้

ลองดูที่การนำไปปฏิบัตินี้และคำอธิบายว่าทำไมจึงเกิดขึ้น

บล็อก Josh Smiths


1

ฉันเพิ่งพบActiveSharp - INotifyPropertyChanged อัตโนมัติฉันยังไม่ได้ใช้ แต่มันก็ดูดี

อ้างจากเว็บไซต์ของมัน ...


ส่งการแจ้งเตือนการเปลี่ยนแปลงคุณสมบัติโดยไม่ระบุชื่อคุณสมบัติเป็นสตริง

เขียนคุณสมบัติเช่นนี้แทน:

public int Foo
{
    get { return _foo; }
    set { SetValue(ref _foo, value); }  // <-- no property name here
}

โปรดทราบว่าไม่จำเป็นต้องรวมชื่อของคุณสมบัติเป็นสตริง ActiveSharp เชื่อถือได้และถูกต้องตัวเลขที่ออกมาเพื่อตัวเอง มันทำงานตามความจริงที่ว่าการใช้งานคุณสมบัติของคุณผ่านเขตข้อมูลสำรอง (_foo) โดยการอ้างอิง (ActiveSharp ใช้การเรียก "by ref" เพื่อระบุว่าเขตข้อมูลสำรองใดถูกส่งผ่านและจากเขตข้อมูลนั้นจะระบุคุณสมบัติ)


1

ความคิดที่ใช้การสะท้อน:

class ViewModelBase : INotifyPropertyChanged {

    public event PropertyChangedEventHandler PropertyChanged;

    bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {

        // Get Name of Property
        string name = mb.Name.Substring(4);

        // Detect Change
        bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);

        // Return if no change
        if (!changed) return false;

        // Update value
        oldValue = newValue;

        // Raise Event
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }//if

        // Notify caller of change
        return true;

    }//method

    string name;

    public string Name {
        get { return name; }
        set {
            Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
        }
    }//method

}//class

นี่มันเจ๋งมากฉันชอบมากกว่าการแสดงออก ข้อเสียควรช้าลง
nawfal

1

ฉันรู้ว่าคำถามนี้มีคำตอบไปแล้วกว่า gazillion แต่ไม่มีพวกเขารู้สึกว่าเหมาะสมสำหรับฉัน ปัญหาของฉันคือฉันไม่ต้องการให้การแสดงยอดนิยมใด ๆ และยินดีที่จะทนกับคำฟุ่มเฟื่อยเล็กน้อยด้วยเหตุผลดังกล่าวเพียงอย่างเดียว ฉันยังไม่สนใจคุณสมบัติอัตโนมัติมากเกินไปซึ่งนำฉันไปสู่โซลูชันต่อไปนี้:

public abstract class AbstractObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
    {
        //Set value if the new value is different from the old
        if (!Source.Equals(NewValue))
        {
            Source = NewValue;

            //Notify all applicable properties
            foreach (var i in Notify)
                OnPropertyChanged(i);

            return true;
        }

        return false;
    }

    public AbstractObject()
    {
    }
}

ในคำอื่น ๆ วิธีการแก้ปัญหาข้างต้นจะสะดวกถ้าคุณไม่รังเกียจที่จะทำสิ่งนี้:

public class SomeObject : AbstractObject
{
    public string AnotherProperty
    {
        get
        {
            return someProperty ? "Car" : "Plane";
        }
    }

    bool someProperty = false;
    public bool SomeProperty
    {
        get
        {
            return someProperty;
        }
        set
        {
            SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
        }
    }

    public SomeObject() : base()
    {
    }
}

ข้อดี

  • ไม่มีการสะท้อนกลับ
  • แจ้งเตือนเฉพาะเมื่อค่าเก่า! = ค่าใหม่
  • แจ้งคุณสมบัติหลายอย่างพร้อมกัน

จุดด้อย

  • ไม่มีคุณสมบัติอัตโนมัติ (คุณสามารถเพิ่มการรองรับได้ทั้งสองอย่าง!)
  • การใช้คำฟุ่มเฟื่อยบางอย่าง
  • ชกมวย (ตีประสิทธิภาพน้อย)

อนิจจามันยังดีกว่าการทำเช่นนี้

set
{
    if (!someProperty.Equals(value))
    {
        someProperty = value;
        OnPropertyChanged("SomeProperty");
        OnPropertyChanged("AnotherProperty");
    }
}

สำหรับสถานที่ให้บริการทุกเดียวซึ่งกลายเป็นฝันร้ายด้วยความฟุ้งเฟ้อเพิ่มเติม ;-(

หมายเหตุฉันไม่อ้างว่าโซลูชันนี้มีประสิทธิภาพที่ดีกว่าเมื่อเปรียบเทียบกับโซลูชันอื่น แต่เพียงว่าเป็นโซลูชันที่ทำงานได้สำหรับผู้ที่ไม่ชอบโซลูชันอื่น ๆ ที่นำเสนอ


1

ฉันมากับคลาสฐานนี้เพื่อใช้รูปแบบที่สังเกตได้สวยมากสิ่งที่คุณต้องการ ( "โดยอัตโนมัติ"การใช้ชุดและรับ) ฉันใช้เวลาหนึ่งชั่วโมงในการสร้างต้นแบบดังนั้นจึงไม่มีการทดสอบหลายหน่วย แต่พิสูจน์แนวคิด หมายเหตุใช้Dictionary<string, ObservablePropertyContext>เพื่อลบความต้องการเขตข้อมูลส่วนบุคคล

  public class ObservableByTracking<T> : IObservable<T>
  {
    private readonly Dictionary<string, ObservablePropertyContext> _expando;
    private bool _isDirty;

    public ObservableByTracking()
    {
      _expando = new Dictionary<string, ObservablePropertyContext>();

      var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
      foreach (var property in properties)
      {
        var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
        {
          Value = GetDefault(property.PropertyType)
        };

        _expando[BuildKey(valueContext)] = valueContext;
      }
    }

    protected void SetValue<T>(Expression<Func<T>> expression, T value)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var originalValue = (T)_expando[key].Value;
      if (EqualityComparer<T>.Default.Equals(originalValue, value))
      {
        return;
      }

      _expando[key].Value = value;
      _isDirty = true;
    }

    protected T GetValue<T>(Expression<Func<T>> expression)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var value = _expando[key].Value;
      return (T)value;
    }

    private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
    {
      var castedExpression = expression.Body as MemberExpression;
      if (castedExpression == null)
      {
        throw new Exception($"Invalid expression.");
      }

      var parameterName = castedExpression.Member.Name;

      var propertyInfo = castedExpression.Member as PropertyInfo;
      if (propertyInfo == null)
      {
        throw new Exception($"Invalid expression.");
      }

      return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
    }

    private static string BuildKey(ObservablePropertyContext observablePropertyContext)
    {
      return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
    }

    private static string BuildKey(string parameterName, Type type)
    {
      return $"{type.Name}.{parameterName}";
    }

    private static object GetDefault(Type type)
    {
      if (type.IsValueType)
      {
        return Activator.CreateInstance(type);
      }
      return null;
    }

    public bool IsDirty()
    {
      return _isDirty;
    }

    public void SetPristine()
    {
      _isDirty = false;
    }

    private class KeyContext
    {
      public string PropertyName { get; set; }
      public Type PropertyType { get; set; }
    }
  }

  public interface IObservable<T>
  {
    bool IsDirty();
    void SetPristine();
  }

นี่คือการใช้งาน

public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
  {
    public ObservableByTrackingTestClass()
    {
      StringList = new List<string>();
      StringIList = new List<string>();
      NestedCollection = new List<ObservableByTrackingTestClass>();
    }

    public IEnumerable<string> StringList
    {
      get { return GetValue(() => StringList); }
      set { SetValue(() => StringIList, value); }
    }

    public IList<string> StringIList
    {
      get { return GetValue(() => StringIList); }
      set { SetValue(() => StringIList, value); }
    }

    public int IntProperty
    {
      get { return GetValue(() => IntProperty); }
      set { SetValue(() => IntProperty, value); }
    }

    public ObservableByTrackingTestClass NestedChild
    {
      get { return GetValue(() => NestedChild); }
      set { SetValue(() => NestedChild, value); }
    }

    public IList<ObservableByTrackingTestClass> NestedCollection
    {
      get { return GetValue(() => NestedCollection); }
      set { SetValue(() => NestedCollection, value); }
    }

    public string StringProperty
    {
      get { return GetValue(() => StringProperty); }
      set { SetValue(() => StringProperty, value); }
    }
  }

1

ฉันแนะนำให้ใช้ ReactiveProperty นี่เป็นวิธีที่สั้นที่สุดยกเว้น Fody

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    ...
    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

แทน

public class Data
{
    // Don't need boiler-plate and INotifyPropertyChanged

    // props
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

( DOCS )


0

ความคิดอื่น ...

 public class ViewModelBase : INotifyPropertyChanged
{
    private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
    protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
        _propertyStore[propertyName] = value;
        OnPropertyChanged(propertyName);
    }
    protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
    {
        object ret;
        if (_propertyStore.TryGetValue(propertyName, out ret))
        {
            return (T)ret;
        }
        else
        {
            return default(T);
        }
    }

    //Usage
    //public string SomeProperty {
    //    get { return GetValue<string>();  }
    //    set { SetValue(value); }
    //}

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
            temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

0

=> นี่คือโซลูชันของฉันพร้อมคุณสมบัติต่อไปนี้

 public ResourceStatus Status
 {
     get { return _status; }
     set
     {
         _status = value;
         Notify(Npcea.Status,Npcea.Comments);
     }
 }
  1. ไม่มีการยืนยันซ้ำ
  2. สัญกรณ์สั้น
  3. ไม่มีสตริงมายากลในรหัสธุรกิจของคุณ
  4. การใช้ซ้ำของ PropertyChangedEventArgs ข้ามแอ็พพลิเคชัน
  5. ความเป็นไปได้ที่จะแจ้งคุณสมบัติหลาย ๆ อันในคำสั่งเดียว

0

ใช้สิ่งนี้

using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;


public static class ObservableFactory
{
    public static T Create<T>(T target)
    {
        if (!typeof(T).IsInterface)
            throw new ArgumentException("Target should be an interface", "target");

        var proxy = new Observable<T>(target);
        return (T)proxy.GetTransparentProxy();
    }
}

internal class Observable<T> : RealProxy, INotifyPropertyChanged, INotifyPropertyChanging
{
    private readonly T target;

    internal Observable(T target)
        : base(ImplementINotify(typeof(T)))
    {
        this.target = target;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;

        if (methodCall != null)
        {
            return HandleMethodCall(methodCall);
        }

        return null;
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;



    IMessage HandleMethodCall(IMethodCallMessage methodCall)
    {
        var isPropertySetterCall = methodCall.MethodName.StartsWith("set_");
        var propertyName = isPropertySetterCall ? methodCall.MethodName.Substring(4) : null;

        if (isPropertySetterCall)
        {
            OnPropertyChanging(propertyName);
        }

        try
        {
            object methodCalltarget = target;

            if (methodCall.MethodName == "add_PropertyChanged" || methodCall.MethodName == "remove_PropertyChanged"||
                methodCall.MethodName == "add_PropertyChanging" || methodCall.MethodName == "remove_PropertyChanging")
            {
                methodCalltarget = this;
            }

            var result = methodCall.MethodBase.Invoke(methodCalltarget, methodCall.InArgs);

            if (isPropertySetterCall)
            {
                OnPropertyChanged(methodCall.MethodName.Substring(4));
            }

            return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
        }
        catch (TargetInvocationException invocationException)
        {
            var exception = invocationException.InnerException;
            return new ReturnMessage(exception, methodCall);
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanging(string propertyName)
    {
        var handler = PropertyChanging;
        if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
    }

    public static Type ImplementINotify(Type objectType)
    {
        var tempAssemblyName = new AssemblyName(Guid.NewGuid().ToString());

        var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
            tempAssemblyName, AssemblyBuilderAccess.RunAndCollect);

        var moduleBuilder = dynamicAssembly.DefineDynamicModule(
            tempAssemblyName.Name,
            tempAssemblyName + ".dll");

        var typeBuilder = moduleBuilder.DefineType(
            objectType.FullName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);

        typeBuilder.AddInterfaceImplementation(objectType);
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
        var newType = typeBuilder.CreateType();
        return newType;
    }
}

}

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.