الربط والأحداث والتنسيق في JavaFX

نمط المراقب في JavaFX

18 دقيقة الدرس 6 من 12

نمط المراقب في JavaFX

نمط المراقب هو العمود الفقري لكل إطار عمل واجهة مستخدم تفاعلية: عندما تتغير قطعة من البيانات، تُخطَر جميع الأطراف المهتمة تلقائيًا — دون أن يضطر أي منها إلى الاستعلام المستمر عن التغييرات. تدمج JavaFX هذا النمط مباشرةً في نظام الخصائص من خلال نوعين متكاملين من المستمعين: InvalidationListener وChangeListener. معرفة أيهما تستخدم، وفهم متى يُطلق كل منهما، مهارة جوهرية لكتابة تطبيقات JavaFX فعّالة.

لماذا نوعان من المستمعين؟

خصائص JavaFX كسولة بحسب التصميم. لا تُعيد الارتباطات المحسوبة حساب قيمتها لحظة تغيُّر إحدى التبعيات؛ بل تُعلّم نفسها بأنها غير صالحة (invalid) وتؤجل إعادة الحساب حتى يطلب أحدٌ القيمةَ فعليًا. هذا الكسل هو نموذج الأداء لنظام الخصائص بأكمله، وينعكس ذلك في دورتَي حياة المستمعَين:

  • InvalidationListener — يُطلق فور انتقال الخاصية من حالة صالحة إلى غير صالحة. لم تُحسب القيمة الجديدة بعد. استخدمه حين تحتاج فقط إلى معرفة أنّ شيئًا تغيّر، لا ماذا تغيّر.
  • ChangeListener — يُطلق فقط حين تُسلّم الخاصية قيمة جديدة فعليًا (أي بعد إعادة الحساب الكسول). يستقبل القيمة القديمة والقيمة الجديدة. استخدمه حين تحتاج إلى التصرف بناءً على القيم المحددة المشاركة في الانتقال.
النموذج الذهني الأساسي: الإبطال رخيص (لا حساب)، والتغيير دقيق (القيمة معروفة). بالنسبة لعناصر التحكم في واجهة المستخدم التي يُعدّلها المستخدم مباشرةً — كـ TextField — نادرًا ما يكون الفرق مهمًا لأن getValue() تُستدعى دائمًا على الفور. أما في سلاسل التبعيات الطويلة من الارتباطات المحسوبة، فيستطيع InvalidationListener تجنّب إعادة الحسابات المتتالية.

InvalidationListener عمليًا

تملك واجهة InvalidationListener الدالية طريقة واحدة: invalidated(Observable observable). تُرفقها بـ addListener(InvalidationListener) وتُزيلها بـ removeListener المقابلة.

import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.SimpleIntegerProperty; public class InvalidationDemo { public static void main(String[] args) { SimpleIntegerProperty count = new SimpleIntegerProperty(0); InvalidationListener listener = (Observable obs) -> { // الخاصية غير صالحة الآن؛ لم نطلب القيمة بعد. System.out.println("count was invalidated"); }; count.addListener(listener); count.set(1); // يُطلق: "count was invalidated" count.set(1); // لا يُطلق — القيمة لم تتغير، لذا لا إبطال count.set(2); // يُطلق: "count was invalidated" // إلغاء التسجيل لتجنب تسريبات الذاكرة count.removeListener(listener); } }

لاحظ أن ضبط الخاصية على القيمة ذاتها للمرة الثانية (set(1) مرتين) لا يُطلق المستمع. تمنع JavaFX الإبطال إذا لم تتغير القيمة — وهذا مهم أن تتذكره حين تتوقع إشعارًا لا يصلك أبدًا.

InvalidationListener وإبطال الارتباط: إذا استدعيت get() داخل InvalidationListener، تعود الخاصية صالحة فورًا وسيُطلق المستمع مرة أخرى عند التغيير التالي. إذا لم تستدع get() مطلقًا، فإن التغييرات اللاحقة على القيمة ذاتها لن تُعيد تشغيل المستمع حتى تتم قراءتها مرة على الأقل. هذا هو سلوك "مرة واحدة غير صالح، تصمت بعدها" في النموذج الكسول.

ChangeListener عمليًا

تعرض واجهة ChangeListener<T> الدالية الطريقة changed(ObservableValue<? extends T> obs, T oldValue, T newValue). داخليًا تستدعي JavaFX get() على الخاصية قبل إرسال الحدث، مما يُجبر التقييم ويضمن لك استلام القيم الفعلية قبل وبعد التغيير.

import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; public class ChangeDemo { public static void main(String[] args) { SimpleStringProperty username = new SimpleStringProperty("alice"); ChangeListener<String> listener = (ObservableValue<? extends String> obs, String oldVal, String newVal) -> { System.out.printf("username changed: '%s' -> '%s'%n", oldVal, newVal); }; username.addListener(listener); username.set("bob"); // username changed: 'alice' -> 'bob' username.set("bob"); // لا حدث — القيمة ذاتها username.set("carol"); // username changed: 'bob' -> 'carol' username.removeListener(listener); } }

مثال واقعي من واجهة المستخدم: التحقق الفوري من النماذج

في تطبيق حقيقي، تُرفق ChangeListener بـ textProperty() الخاصة بـ TextField لمنح المستخدم ردود فعل فورية. تتيح لك القيمة القديمة تطبيق منطق "التراجع عن التغيير الأخير" عند الحاجة.

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Stage; public class LiveValidationApp extends Application { @Override public void start(Stage stage) { TextField emailField = new TextField(); emailField.setPromptText("Enter email address"); Label feedback = new Label(); // إرفاق ChangeListener بخاصية النص emailField.textProperty().addListener((obs, oldText, newText) -> { if (newText.contains("@") && newText.contains(".")) { feedback.setText("Valid email"); feedback.setTextFill(Color.GREEN); } else { feedback.setText("Invalid email"); feedback.setTextFill(Color.RED); } }); VBox root = new VBox(8, emailField, feedback); root.setStyle("-fx-padding: 20;"); stage.setScene(new Scene(root, 320, 120)); stage.setTitle("Live Validation"); stage.show(); } public static void main(String[] args) { launch(args); } }

الاختيار بين النوعين

  • استخدم InvalidationListener حين: تحتاج فقط إلى تشغيل تأثير جانبي (مثل تعليم ذاكرة تخزين مؤقت بأنها قديمة، أو جدولة إعادة رسم) ولا تحتاج إلى معرفة القيم المحددة قبل وبعد. إنه أرخص لأنه يتجنب إجبار تقييم الخاصية.
  • استخدم ChangeListener حين: تحتاج إلى القيمة القديمة أو الجديدة، أو تحتاج إلى إجراء مقارنات أو تسجيل أو تحقق يعتمد على القيم الفعلية.
  • بالنسبة لـعناصر التحكم في واجهة المستخدم التي يقرأ المستخدم قيمتها ويكتبها باستمرار، يُفضَّل ChangeListener — تكلفة إعادة حساب خاصية بسيطة لا تُذكر، والكود أوضح.

تجنب تسريبات الذاكرة: المستمعون الضعفاء

يُنشئ إضافة مستمع مرجعًا قويًا من الخاصية إلى المستمع (وغالبًا إلى كائن المتحكم أو النموذج بشكل غير مباشر). إذا أضفت مستمعًا لكنك لم تستدعِ removeListener أبدًا، تحتفظ الخاصية بالمستمع حيًا طوال عمر الخاصية نفسها — وهو ما قد يكون غير محدود في التطبيقات طويلة الأمد.

توفر JavaFX كلًا من WeakInvalidationListener وWeakChangeListener في حزمة javafx.beans. يلفّان مستمعك في WeakReference؛ تُنظّف الخاصية تلقائيًا بعد جمع المهملات للمستمع الأساسي.

import javafx.beans.WeakInvalidationListener; import javafx.beans.property.SimpleDoubleProperty; public class WeakListenerDemo { // احتفظ بمرجع قوي في الكائن المحيط حتى لا يُجمع فورًا private final javafx.beans.InvalidationListener strongRef; public WeakListenerDemo(SimpleDoubleProperty price) { strongRef = obs -> System.out.println("Price invalidated"); // تحتفظ الخاصية بـ WeakReference فقط — لا تسريب price.addListener(new WeakInvalidationListener(strongRef)); } }
الفخ الشائع — المستمع الضعيف المُجمَّع فورًا: إذا أنشأت المستمع كـ lambda مجهولة ولففتها فورًا في WeakInvalidationListener دون تخزين المرجع القوي في أي مكان، قد يجمعها مُجمّع المهملات في دورة الجمع التالية وسيتوقف مستمعك عن العمل بصمت. احفظ دائمًا مرجعًا قويًا للمستمع الأساسي في حقل الكائن المحيط.

الخلاصة

يمنحك نمط المراقب في JavaFX نقطتَي إشعار دقيقتَين في دورة حياة الخاصية. يُطلق InvalidationListener مبكرًا وبتكلفة منخفضة عند لحظة "ربما تغيّر شيء". ويُطلق ChangeListener لاحقًا مع قيم مضمونة قبل وبعد التغيير. استخدم المتغيرات الضعيفة للخصائص طويلة العمر في المتحكمات والنماذج للحفاظ على نظافة الذاكرة. في الدرس التالي سترى كيف تُطبّق هذه المعرفة عند بناء عناصر تحكم مخصصة قابلة لإعادة الاستخدام تكشف حالتها القابلة للمراقبة.