ثبت داده ها (Data Binding) از طریق عبارت های درختی در WPF

شنبه 1 اسفند 1394

در هنگام استفاده از فریم ورک WPF، میبینیم که بسیاری از مفاهیم، با استفاده از binding در زمان اجرا، بخش مشخصی از آنها به طور امن در زمان کامپایل و بقیه به صورت استاتیک کنترل می شود. یک مثال جامع برای این مورد، data binding است. در این مقاله می خواهیم پیاده سازی data bindig از طریق عبارات درختی(Expression Trees) را نشان دهیم.

ثبت داده ها (Data Binding) از طریق عبارت های درختی در WPF

 ساختار Binding در Xaml به صورت زیر است:

<TextBlock Text="{Binding Path=SomeTextProperty}"/>

این ساختار یک روش نسبتا کوتاه برای بیان Property کنترل هایی است که باید به Property دیگر در مدل یا ViewModel برنامه Bind شوند. اما رشته ModelProperty برای کلاس مدل مورد نظر، تا زمان اجرا اعتبار ندارد. بنابراین اگر مثلا ModelProperty  را با عنوانی مانند SchmodelProperty از دست بدهیم، آن را تا زمانی که UI شامل binding عمل نکند، نمی توانیم پیدا کنیم. در ادامه در این مقاله یکی از روش های ممکن برای Data Binding را توضیح داده و در نهایت Binding را به صورت زیر انجام می دهیم:

new TextBlock() { Text = model.SomeTextProperty }

برای یک مقدار واقعی این فقط یک experssion یا یک عبارت است که یک آبجکت TextBlockجدید با یک Property مربوط به text را برمی گرداند که مقدار Property تعریف شده، توسط مدل تنظیم می شود. با استفاده از یک ویژگی شناخته شده در #C با نام عبارت های درختی(Expression Trees)، این امکان وجود دارد که عبارت بالا به عبارتی تبدیل شود که قابلیت مشابه با قطعه کد XAML  در بالا را دارد.

پیش زمینه

بخش عمده ای از این مقاله در مورد تبدیل عبارات درختی می باشد اما  از برخی دستورات Linq هم استفاده می شود. هر دو این موضوعات نسبتا عمیق هستند و برای درک آن شناخت قبلی لازم است. البته درک اجمالی از مفهوم MVVM در WPF نیز مفید می باشد.

در سرتاسر مقاله عبارت model  و ViewModel به صورت مترادف استفاده می شوند و تفاوت آنها در این مقاله مهم نیست.

 

اعلام تغییرات(Change Notification)

روش استاندارد انتشار تغییرات از مدل به UI در WPF ، با استفاده از اینترفیس IPropertyNotifyChanged انجام می شود. این اینترفیس به یک رویداد برای راه اندازی نیازمند است، یعنی هر زمان که یک Property در مدل تغییر کرد، راه اندازی می شود و رویداد داده شامل نام Property و به صورت String است. تا اینجا همه چیز خوب است اما این مورد، ویژگی دیگری هم ارائه می دهد که اطلاعات شناخته شده در زمان کامپایل(compile-time) کنار گذاشته شده و در زمان اجرا(run-time) برای اعتبار سنجی استفاده می شود.

یک مدل رایج برای توسعه کلاس پایه، پیاده سازی متد OnPropertyChanged می باشد که کلاس های مشتق شده از آن را برای بالا آوردن رویداد خاصی، فراخوانی می کند. متاسفانه پارامتر propertyName  باید به صورت رشته مشخص شود که می تواند به راحتی توسط برنامه نویس اشتباه نوشته شود.

 تفاوت های کار به این روش به صورت زیر است:

استفاده از ابزارهای code-generation در زمان Build برای ایجاد اتوماتیک خصوصیت notifying 

استفاده از عبارات lambda و extension method ها برای کاهش کد مورد نیاز

استفاده از صفت CallerMemberName

اگرچه اینها روش های خوبی برای مدیریت هستند اما ما می خواهیم روش دیگری را امتحان کرده و یک کلاس <Property<T تعریف کنیم که می تواند notification را به صورت اتوماتیک تغییر دهید و برای استفاده راحتتر است.

پیاده سازی این کد با مکانیسم WPF data binding سازگار نیست با این حال از آنجایی که این روش به صورت آزمایشی است ما هیچ تردیدی برای امتحان آن نداریم . رویکرد کلی به این صورت پیاده سازی می شود که به جای INotifyPropertyChanged کار می کند اما کمی پیچیده تر است. پیاده سازی <property<T در زیر نمایش داده شده است:

public class Property<T>
{
    public event EventHandler<ValueChangedEventArgs<T>> ValueChanged;

    public static implicit operator T(Property<T> p)
    {
        return p.val;
    }

    public Property(T value)
    {
        val = value;
    }

    public Property()
    {
        val = default(T);
    }

    private T val;
    public T Value
    {
        get { return val; }
        set
        {
            if (!EqualityComparer<T>.Default.Equals(value, val))
            {
                val = value;
                ValueChanged.Raise(this, new ValueChangedEventArgs<T>(val));
            }
        }
    }
}

عملگر T بازنویسی شده، به صورت اختیاری است اما به نوع <Property<T اجازه می دهد که به طور مستقیم در عبارات انتساب(assign) استفاده شود. برای مثال model را فرض کنید که در آن Text به صورت <Property<string می باشد:

textBox.Text = model.Text

با استفاده از نوع <Property<T، کلاس model می تواند به صورت زیر ساده شود:

public class Model
{
    // Assign an initial value to the Property<string>
    public Property<string> Text = new Property<string>("initial value");

    public void Method()
    {
        // Assign the Property<string>'s value, which causes the ValueChanged
        // event to be fired.
        Text.Value = text;
    }
}

 

کار با عبارات درختی(Expression Trees)

قبل از بررسی تبدیلات عبارات درختی استفاده شده برای data binding، می خواهیم در مورد ساختار بلوک های انتزاعی صحبت کنیم. این ساختار می تواند به طور گسترده برای انواع مختلف مرتب سازی queryها یا تبدیل درختان مورد استفاده قرار گیرد. برای ایجاد آنها از برخی عملگرهای Linq استاندارد استفاده می کنیم بنابراین می توانیم از query syntax موجود در C# بهره ببریم. اینها بخش هایی هستند که حول کلاس ExpressionVisitor موجود در فضای نام System.Linq.Expressions ایجاد می شوند. این کلاس تمام منطق مورد نیاز برای تبدیل عبارات درختی(transform experssion tree) را دارد، و حتی بدون آگاهی از انواع مختلف گره های عبارت(expression node) می تواند فراهم شود. متاسفانه استفاده مستقیم از ExpressionVisitor نیازمند یک پیاده سازی مشخص از کلاس مشتق از آن شده است. عملگرهای Linq ارائه شده، اجازه می دهد که انواع مختلف عملگرها بدون پیاده سازی یک کلاس سفارشی استفاده شوند.

عملگرهای Linq برای ساختارهای درختی

این روش معمولا زمان نوشتن عملگرهای Linq استفاده می شود و یک اینترفیس <ISomeInterface<T را شناسایی می کند که از مفاهیم مشترک تمام عملگرهایی که می خواهیم از آنها پشتیبانی کنیم استفاده می کند. در این تغییرات عبارات درختی دو عملگر بنیادی مشخص کرده ایم که برای تحولات مشخص متفاوت رایج است:

ملاقات گره های درخت به صورت بالا به پایین

ملاقات گره های درخت به صورت پایین به بالا

 

تنها تفاوت بین این دو به این است که در گره های ملاقات شده، والد قبل از فرزندان باشد یا بالعکس. مزایای ملاقات از بالا به پایین این است که زیر درختانی که در تبدیل عبارت مشارکت ندارند می توانند نادیده گرفته شوند. مثلا اگر چیزی وجود دارد که در مورد نود ریشه مشخص شده باشد نشان می دهد که نود های دیگر نیازی به بررسی ندارند، با این کار می توانید از تعداد بسیار زیاد پردازش ها صرف نظر کنید. به عبارت دیگر با پردازش پایین به بالا، خیلی طول می کشد که از یک زیر درخت مشخص پرید زیرا زمانی که شما به آن برسید  از قبل تمام فرزندان ها آن را ملاقات کرده اید.

مزیتی که برای ملاقات پایین به بالا وجود دارد این است که برای انواع خاصی از تبدیلات استفاده می شود. با این حال اگر یک نود والد را با یک نود جدید جایگزین کنید که شامل نود اصلی به عنوان فرزند می باشد، می توانید به راحتی یک کد بی پایان ایجاد کنید، پس از گسترش درخت باید نودهای والد اصلی را مجدد ملاقات کنید. برای درک بهتر یک مثال بسیار ساده را بررسی می کنیم. با این درخت شروع کنید که XZY نامیده می شود:

 X
/ \
Y Z

و نودها را از بالا به پایین ملاقات می کند، Z را با درخت XZY جایگزین کنید. درخت زیر بعد از تعداد کمی ملاقات اولیه از نود Z نتیجه می دهد:

 X       X         X
/ \     / \       / \
Y X     Y X       Y X
 / \     / \       / \
 Y Z     Y X       Y X
          / \       / \
          Y Z       Y X
                     / \
                     Y Z

می توانید مشاهده کنید که این الگو به صورت بی نهایت ادامه خواهد داشت. اما به جای حالت پایین به بالا در این درخت تنها با یک جایگزینی می توانیم آن را متوقف کنیم:

 X
/ \
Y X
 / \
 Y Z

در بالا از درختان دودویی استفاده کرده ایم (گره هایی تنها با دو فرزند) اما توجه داشته باشید که درختان عبارت در C# ، گره هایی با انواع مختلفی دارند. بعضی از آنها یک تعداد ثابت گره های فرزند دارند(برای مثال عبارات دودویی) اما برخی دیگر تعداد گره های متغیر دارند(برای مثال آرگومان ها در عبارت فراخوانی یک متد)

 

مقایسه با IEnumerable

از آنجایی که عملگرهای ساخته شده در linq به خوبی مناسب اینترفیس <IEnumerable<T می باشد به همین دلیل اغلب اوقات برای مقایسه مجموعه ای از عملگرهای سفارشی به صورت <IEnumerable<T مناسب است. در این مقاله ما فرایند visitation یا ملاقات را برای enumeration مورد بررسی قرار می دهیم. آنها تا حدودی شبیه به هم هستند و در واقع به طور طبیعی برای تصور یک نوع <IEnumerable<T استفاده می شوند. با این حال ما می خواهیم ساختار درخت را حفظ کنیم زیرا ممکن است این ساختار برای تبدیلات مهم باشد. ( مگر اینکه یک نود تکی فقط با نوهای تکی جایگزین شود). روش دیگر این است که برخی ساختارهای درختی متفاوت می تواند یک <IEnumerable<T مشابه با تولید گره ها روی نظم خاص داشته باشند.

اگرچه از مدل <IEnumerable<T به عنوان شروع استفاده می کنیم اما اجازه دهید یک مجموعه از عملگرهای LINQ query را هم در نظر بگیریم که توابع روی نودها در درخت را به کار می گیرد. به جای اینترفیس enumerable می خواهیم از الگوی ملاقات کننده معمول استفاده کنیم. که برای هر نود وجود دارد. ما می خواهیم یک نوعی را ارائه دهیم که می تواند هر نوعی نودی را بپذیرد. به طور معمول این یک کلاس است که اینترفیس در زمینه خاصی را پیاده سازی می کند ویک متد دارد که برای هر نوع نودی تعریف شده است. ممکن است در این نقطه تعجب کنید که چگونه این می تواند با عملگرهای Linq یکپارچه شود.

بعد از تمام موارد بالا، عملگرهای Linq توابعی را روی اکثر انواع T  به کار می گیرند و اینترفیس به صورت دلخواه پیاده سازی نمی شود. با این حال این دو مفهوم می توانند با یکدیگر توسط مزایایی که دارند، در کنار هم باشند. همچنین توجه کنید که تمام نودها در یک عبارت درختی یک کلاس پایه مشترک دارند. با توجه به این رخداد خوب، می توانیم یک اینترفیس با متدهای AcceptExpressionTypeXyz را به یک تابع تکی کاهش دهیم که روی بیشتر انواع Expression کار می کند.

مجموعه عملگرهای LINQ

در اینجا مجموعه ای از عملگرها را ارائه می دهیم که در کد پیاده سازی شده اند و برای تبدیل عبارت های درختی استفاده می شوند. قبل از لیست کردن عملگرهای خود، باید اینترفیس مشترک را تعریف کنیم که visitation semantics را نگه می دارد. ما این اینترفیس را <IVisitable<T می نامیم که T  یک Expression یا یک نوعی می باشد که از آن ناشی می شود(یا حتی هر نوعی که در برخی موارد به عملیات select نهایی در query expression محدود می شوند، که قابل انتساب هستند). تعریف <IVisitable<T در زیر نمایش داده شده است:

public interface IVisitable<T>
{
    Expression Visit(Func<T, Expression> accept);
}

IVisitable<T یک متد دارد که یک تابع از T را برای Expression بکار می گیرد و یک عبارت تبدیل بر می گرداند. این به طور ضمنی است که پیاده سازی <IVisitable<T شامل یک رفرنس برای برخی source expression جهت تبدیل می باشد.

در ادامه ما نیاز داریم تعدادی توابع داشته باشیم که اشیاء Expression  می گیرد و آنها را به انواعی که  IVisitable<T>  را پیاده سازی کرده برمیگرداند. این همان جایی است که می توان مفهوم پیمایش را حفظ کرد:

public static IVisitable<Expression> TopDown(this Expression e);
public static IVisitable<Expression> BottomUp(this Expression e);

هرکدام از اینها یک <IVisitable<T برمی گردانند که Expression  ارائه شده را به منظور مناسب، ملاقات می کنند. چیزی که باید توجه داشته باشید، این است که در اینجا این روش یک بار به طور مستقیم انتخاب شده و تمام عملگرهای Linq مورد استفاده قرار می گیرد تا <IVisitable<T برگردانده شده به همان جهت محدود شوند. این با یک LINQ query expression می تواند در یک جهت استفاده شود(مگر این که یک Query جداگانه به جای تو در تو استفاده شود).

حالا برای عملگرها:

IVisitable<TResult> Select<T, TResult>(this IVisitable<T> source, Func<T, TResult> selector)

یک <IVisitable<TResult جدید برمیگرداند که زمانی که ملاقات شود، نودها را به عنوان انتخاب گر تبدیل شده دریافت می کنند.

IVisitable<T> Where<T>(this IVisitable<T> source, Func<T, bool> predicate)

یک <IVisitable<T جدید برمیگرداند که زمانی که ملاقات می شوند، نودها را به نوع تبدیل شده توسط انتخاب گر ارائه شده، دریافت می کند. یک <IVisitable<T برمیگرداند که از روی هر نودی می پرد(اما نه فرزندان آنها) که برای آن گزاره False برمی گرداند.

IVisitable<T> OfType<T>(this IVisitable<Expression> source)

یک <IVisitable<T جدید برمی گرداند که از روی نودهایی میپرد(اما نه فرزندان آنها) که از نوع T نباشند و همچنین نودها را  قبل از ارسال آن به Func<T, Expression> delegate ، در طول ملاقات، به نوع T کست(Cast) می کند.

IVisitable<Expression> TakeWhile(this Expression source, Func<Expression, bool> predicate)

این بسیار شبیه به عملگر where می باشد اما این از روی نودهای فرزندی می پرد که با گزاره مطابقت ندارند. اگر بخواهید از روی تمام زیر درختان بپرید(Skip)، این عملگر مناسب است.

Expression AsExpression<T>(this IVisitable<T> visitable)

شیئ Expression را برمیگرداند که احتمالا تبدیل شده است.

List<T> ToList<T>(this IVisitable<T> visitable)

تبدیلات Data binding

ما در حال حاضر آماده نیستیم که تبدیل عبارات درختی را آغاز کنیم. مسلما روش های جایگزین و انتخاب های طراحی بسیاری در این زمینه وجود دارند. ما در اینجا با کنترل های WPF کار می کنیم. همانطور که در مقدمه ذکر شد، ایده اساسی پشت تبدیل عبارات، گرفتن عبارت از این فرم و برگرداندن آن به یک عبارت جدید از همین فرم است:

new SomeWpfControl() {
  // ... member initialization expressions ...
}

شبه کد:

{
    var control = new SomeWpfControl() { ... };
    foreach (memberInitExpression in newExpression)
    {
        foreach (modelProperty in memberInitExpression)
        {
            modelProperty.ValueChanged += (sender, args) => evaluate(memberInitExpression)
        }
    }
    return control;
}

این نصب رویداد ValueChanged برای هر Property در مدل است که بخشی از مقدار دهی اولیه property یک کنترل می باشد. هر زمان که هر یک از این Property های مدل به روز رسانی شوند، initializer expression دوباره به منظور به روز رسانی خصوصیت کنترل ها مورد ارزیابی قرار می گیرد. قبل از آن می توانیم نگاهی به تبدبل نهایی در کد بیاندازیم. هردو، ویژگی های ارزشمند هستند اما برای اضافه کردن برخی پیچیدگی ها پیاده سازی می شود.

 

دامنه محدود شده جهت ارزیابی مجدد (limited Re-evaluation Scope)

این یک مرتب سازی بر اساس بهینه سازی است که برای مدیریت بهتر در نظر گرفته شده است و شامل عبارت های مقدار دهی شده مانند کنترل های WPF است. یک مثال که در زمان استفاده از یک دکمه سفارشی است به صورت زیر است:

 

new Button() {
    Enabled = model.Enabled,
    Content = new TextBlock() {
        Text = model.Text
    }
}

ایده در اینجا این است، زمانی که model.Text تغییر می کند لازم نیست TextBlock دوباره ایجاد شود حتی اگر در نهایت بخشی از یک عبارت استفاده شده برای اختصاص به یک خصوصیت Button.Content باشد. این چیزی نیست که به طور کلی برای عبارت های دلخواه true باشد، این یک فرض معقول است که هر نوع تغییر مدیریتی که لازم باشد را زمانی که کنترل فرزند به روزرسانی شود از قبل توسط کنترل های خود مدیریت می کند.

نتیجه نهایی این است که زمانی که به دنبال وابستگی های مدل در عبارات انتساب برای WPF می گردیم کنترل ها صرف نظر می شوند.

در source code یک متد وجود دارد

Dependencies<TSkip>(MemberBinding binding, Type dependType) 

که این ایده را پیاده سازی کرده است. این کار با استفاده از عملگر TakeWhile به منظور صرف نظر کردن عبارات جدید قرار داده شده و از نوع TSkip می باشد. در این مقاله TSkip یک UIElement است و dependType یک typeof(Property<>) می باشد.

الگوی رویداد Weak

این الگوی طراحی معمولا در هر زمان که رویداد مشترک اتفاق بیفتد طول عمر کوتاهتری  نسبت به نشر دهندگان مرتبط با آنها دارد. این یک نوع ایراد حافظه است که می تواند در کنترل های WPF که دوباره ایجاد شده اند اتفاق بیفتد در حالی که event handler ها ثبت شده باشند. زیرا این handler یک رفرنس به این کنترل دارد و (زمان استفاده از مکانیسم ثبت رویداد استاندارد برای مثال عملگر +) این مدل یک رفرنس به handler دارد.

زمان استفاده از الگوی Weak Event( به صورت WPF) یک کلاس پراکسی، handler ها را با یک publisher ثبت می کند و جدول خود از رفرنس های weak را برای handler ها حفظ می کند بنابراین این از جمع آوری اطلاعات بیهوده جلوگیری نمی کند. کتابخانه NET 4.5. یک کلاس WeakEventManager فراهم می کند که می توانیم از آن استفاده کنیم(TSource، TEventArgs). با این حال به خاطر این که ما از Lambda به عنوان handler استفاده می کنیم باید جدول خود از رفرنس ها قوی برای مدیریت رویدادها حفظ کنیم.

در غیر این صورت Handler بلافاصله برای garbage collection واجد شرایط می شود.برای انجام این کار ما یک کلاس ایجا می کنیم که نام آن BoundContent قرار دارد و از System.Windows.Controls.ContentControl ارث بری می کند. این کنترل محتوای خاص یک عبارت را به کد کامپایل شده و event handler های جدول تبدبل می کند.

تبدیلات

بخش عمده ای از کد برای تبدیل عبارت است که event handlers را وارد می کند در زیر نشان داده شده است. این کد کمی طولانی است اما عملگرهای تعریف شده در بالا را نشان می دهد.

public static BoundExpression<TResult> AddBindings<TBase, TResult>(this Expression<Func<TResult>> expr)
{
    // Generate a visitable that visits only MemberInitExpressions 
    // for type T, i.e., expressions of the form "new T(){ ... }".
    // It is very important to use BottomUp() so that nodes can be 
    // replaced without visiting the new nodes.
    var visitable =
        from e in expr.BottomUp()
        where e.IsMemberInit<TBase>()
        select e as MemberInitExpression;

    // This list is used to store references to the handlers that are 
    // created when the expression is evaluated.
    var table = new BindingTable();
    int handlerIndex = 0;

    // Transform each MemberInitExpression into a new expression that,
    // when evaulated, adds ValueChanged handlers which update the 
    // object created by the new expression.
    var modifiedExpression = visitable.Visit(init =>
    {
        // Create a variable that will store the constructed object.
        var newObjectVar = Expression.Variable(init.NewExpression.Type);

        // For each member binding (e.g., assignment expression 
        // within the braces), enumerate all the sub-expressions that 
        // are of a Property<T> type.  For each of the Property<T> 
        // expressions, create a lambda expression that will serve as 
        // a ValueChanged event handler.
        var handlerRegistrations =
            from binding in init.Bindings
            from depend in binding.Dependencies<TBase>(typeof(Property<>))

            // (object sender, ValueChangedEventArgs<T>) formal parameters
            let sourceParameter = Expression.Parameter(typeof(object))
            let valueType = typeof(ValueChangedEventArgs<>)
                .MakeGenericType(depend.Type.GenericTypeArguments[0])
            let valueParameter = Expression.Parameter(valueType)

            // (sender, args) => ObjectProperty = expression;
            let lambda = Expression.Lambda(
                typeof(EventHandler<>).MakeGenericType(valueType),
                Expression.Block(
                    Expression.Assign(
                        Expression.Property(newObjectVar, binding.Member.Name),
                        // TODO: This only works for MemberAssignment bindings
                        ((MemberAssignment)binding).Expression)
                ),
                sourceParameter, valueParameter)

            // MethodInfo for WeakEventManager<...>.AddHandler method
            let addHandler = typeof(System.Windows.WeakEventManager<,>)
                .MakeGenericType(depend.Type, valueType)
                .GetMethod("AddHandler")

            // Create a block expression that 
            //  1. stores the handler in a variable
            //  2. adds it to the handlers list
            //  3. registers it using the WeakEventManager
            let lambdaVar = Expression.Variable(lambda.Type)

            select Expression.Block(
                new ParameterExpression[] { lambdaVar },
                Expression.Assign(lambdaVar, lambda),
                Expression.Call(
                    Expression.Constant(table),
                    typeof(BindingTable).GetMethod("AddHandler"),
                    Expression.Constant(handlerIndex++),
                    lambdaVar),
                Expression.Call(
                    addHandler,
                    depend,
                    Expression.Constant("ValueChanged"),
                    lambdaVar));

        // Finally, convert the original expression into a block expression that:
        //  1. Evaluates the original new T(){...} expression and stores the result in a variable.
        //  2. Registers handlers on the Property<T> objects
        //  3. Returns the new object (just as the original expression did)
        var statements = new List<Expression>();
        statements.Add(Expression.Assign(newObjectVar, init));
        statements.AddRange(handlerRegistrations);
        statements.Add(newObjectVar);

        return Expression.Block(
            new ParameterExpression[] { newObjectVar },
            statements);
    });

    return new BoundExpression<TResult>(
        (Expression<Func<TResult>>)modifiedExpression, table);
}

 

مثال

به منظور نشان دادن آنچه که تا کنون یاد گرفته ایم یک اپلیکیشن WPF با استفاده از کنترل BoundControl ایجاد کرده ایم. برنامه را اجرا کنید و خروجی را ببینید:

 

 

این پنجره کمتر با کاربر در تعامل است و بیشتر برای نشان دادن برخی اطلاعات اولیه در مورد data binding استفاده می شود. 

Pretty boring stuff  به عنوان UI استفاده می شود اما همه در C# و با نوعی روش امن انجام می شود. expression  برای اعلان UI مانند به زیر استفاده می شود(چیزی به طور معمول در XAML رخ می دهد):

public MainWindow()
{
    InitializeComponent();

    var viewModel = new ViewModel()
    {
        Editable = { Value = false },
        Text = { Value = "asdf" }
    };

    grid.Children.Add(new BoundContent(() =>
        new DockPanel().Containing(
            new Button() {
                Content = viewModel.Editable ? 
                    (object)new Border() {
                        BorderThickness = new Thickness(5),
                        BorderBrush = Brushes.Blue,
                        Child = new TextBlock() {
                            Text = "Disable edit",
                            FontStyle = FontStyles.Italic
                        }
                    }
                    :
                    new TextBlock() {
                        Text = "Enable edit"
                    }
            }
            .DoWith(c => DockPanel.SetDock(c, Dock.Top))
            .OnClick(c => viewModel.Toggle()),

            new Button() {
                Content = "Reset text"
            }
            .DoWith(c => DockPanel.SetDock(c, Dock.Top))
            .OnClick(c => viewModel.ResetText()),

            new TextBlock() {
                Text = viewModel.Text.Value.Length + " characters"
            }
            .DoWith(c => DockPanel.SetDock(c, Dock.Top)),

            new TextBox() {
                Text = viewModel.Text,
                IsReadOnly = !viewModel.Editable,
                Background = viewModel.Editable ?
                    Brushes.LightBlue : 
                    Brushes.LightGreen
            }
            .OnTextChanged((s, args) => viewModel.Text.Set(s.Text))
        )));
}

 

ایده متد  extension می باشد که در اینجا پیاده سازی شده است. تمام این متد شیئ را به یک Action delegate ارسال کرده و همان شیئ را برمیگرداند. این کار از ذخیره یک جای موقت به منظور فراخوانی متد DockPanel.SetDock جلوگیری می کند.

در کد زیر توجه کنید که چگونه بسیاری از خصوصیت ها با استفاده از expression ها تنظیم شده اند:

Background = viewModel.Editable ?
    Brushes.LightBlue : 
    Brushes.LightGreen

به منظور بدست آوردن چیزهای مشابه در XAML شما باید کارهایی شبیه به زیر انجام دهید:

<Style TargetType="TextBox">
  <Setter Property="Background" Value="LightGreen"/>
  <Style.Triggers>
    <DataTrigger Binding="{Binding Editable}" Value="True">
      <Setter Property="Background" Value="LightBlue"/>
    </DataTrigger>
  </Style.Triggers>
</Style>

 

 

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان
  • WPF
  • 2k بازدید
  • 2 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید