ربط الخصائص
في الدرس السابق رأيت أن كل قيمة واجهة مستخدم في JavaFX — موضع شريط التمرير، نص حقل الإدخال، حالة خانة الاختيار — تُغلَّف في كائن Property بدلًا من تخزينها في حقل عادي. ويوجد هذا الغلاف لسبب واحد تحديدًا: حتى تتمكن أجزاء مختلفة من تطبيقك من الارتباط به والبقاء متزامنة تلقائيًا دون أي حلقة تحديث يدوية.
يغطي هذا الدرس وضعَي الربط الأساسيَّين اللذين يجب على كل مطور JavaFX معرفتهما: الربط أحادي الاتجاه والربط ثنائي الاتجاه.
الربط أحادي الاتجاه: bind()
يُعلن الربط أحادي الاتجاه أن الخاصية A يجب أن تعكس دائمًا القيمة الحالية للخاصية B. عند تغيير B تُحدَّث A فورًا وتلقائيًا. لا يمكن ضبط A مباشرةً طالما أنها مرتبطة — فمحاولة ذلك تُلقي استثناءً في وقت التشغيل.
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class OneWayDemo {
public static void main(String[] args) {
DoubleProperty source = new SimpleDoubleProperty(10.0);
DoubleProperty target = new SimpleDoubleProperty();
// سيساوي target دائمًا source
target.bind(source);
System.out.println(target.get()); // 10.0
source.set(42.0);
System.out.println(target.get()); // 42.0 — تحديث تلقائي
// target.set(99.0); // RuntimeException: لا يمكن ضبط قيمة مرتبطة.
}
}
مثال حقيقي أكثر وضوحًا: إبقاء شريط التقدم متزامنًا مع خاصية نسبة التحميل في نموذج العرض:
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class BindingApp extends Application {
@Override
public void start(Stage stage) {
Slider slider = new Slider(0, 1, 0.5);
ProgressBar bar = new ProgressBar();
// أحادي الاتجاه: يعرض الشريط دائمًا ما يُبلّغ عنه شريط التمرير
bar.progressProperty().bind(slider.valueProperty());
VBox root = new VBox(10, slider, bar);
root.setPadding(new javafx.geometry.Insets(20));
stage.setScene(new Scene(root, 300, 120));
stage.setTitle("عرض الربط أحادي الاتجاه");
stage.show();
}
}
اسحب شريط التمرير وسيتتبعه شريط التقدم — دون سطر واحد من معالجة الأحداث. يتولى إطار الربط التوصيل بالكامل.
الاتجاه مهم: target.bind(source) يجعل target يعتمد على source. إذا كتبتها بالعكس — source.bind(target) — انعكست التبعية. قلها دائمًا بصوت عالٍ: "الهدف يتبع المصدر."
إلغاء الربط
استدع unbind() لتحرير خاصية من ربطها. بعد ذلك يمكن ضبطها بحرية من جديد:
bar.progressProperty().unbind();
bar.progressProperty().set(1.0); // مسموح الآن
الربط ثنائي الاتجاه: bindBidirectional()
أحيانًا يجب أن تظل خاصيتان متساويتَين دائمًا وبإمكان أي من الجانبين التغيير. المثال الكلاسيكي هو حقل نص ونموذج بيانات خلفي: يكتب المستخدم في الحقل فيجب أن يتحدث النموذج؛ ويضبط التطبيق النموذج فيجب أن يتحدث الحقل. هذا هو الربط ثنائي الاتجاه.
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BiDirectionalDemo {
public static void main(String[] args) {
StringProperty modelName = new SimpleStringProperty("Alice");
StringProperty fieldText = new SimpleStringProperty();
modelName.bindBidirectional(fieldText);
// النموذج يقود الحقل
System.out.println(fieldText.get()); // "Alice"
// الحقل يقود النموذج
fieldText.set("Bob");
System.out.println(modelName.get()); // "Bob"
// النموذج يقود الحقل مجددًا
modelName.set("Charlie");
System.out.println(fieldText.get()); // "Charlie"
}
}
في واجهة مستخدم حقيقية توصّل TextField بخاصية في نموذج العرض:
TextField nameField = new TextField();
StringProperty viewModelName = new SimpleStringProperty("Alice");
// أي جانب يتغير والآخر يتابعه دائمًا
nameField.textProperty().bindBidirectional(viewModelName);
فضّل الربط ثنائي الاتجاه لحقول النماذج. يُلغي الحاجة إلى كتابة ChangeListener على الحقل ومستمع منفصل على النموذج. كلا الاتجاهين تُغطيهما سطر واحد.
إلغاء الربط ثنائي الاتجاه
استخدم unbindBidirectional() — يجب أن تمرر نفس كائن الخاصية الذي استُخدم عند الربط:
nameField.textProperty().unbindBidirectional(viewModelName);
// الآن تتغير كلتا الخاصيتين باستقلالية
خطر تسريب الذاكرة: تحتفظ الروابط ثنائية الاتجاه بمراجع قوية لكلتا الخاصيتَين. إذا استبدلت أحد الكائنات (مثلًا عند تحميل كيان جديد في النموذج) دون استدعاء unbindBidirectional() أولًا، تظل الخاصية القديمة حية في الذاكرة. ألغِ الربط دائمًا قبل إعادة الربط بكائن جديد.
أحادي الاتجاه مقابل ثنائي الاتجاه — متى تستخدم أيًّا منهما
- أحادي الاتجاه (
bind) — استخدمه عندما يجب أن تتبع قيمة مشتقة مصدرها دون أن يُسمح بتغيير الجانب المشتق مباشرةً. أمثلة: تسمية تعرض قيمة شريط تمرير؛ زر معطّل حين يكون حقل النص فارغًا؛ إجمالي محسوب يعكس خاصية الكمية.
- ثنائي الاتجاه (
bindBidirectional) — استخدمه حين يجب أن يظل عنصرا واجهة مستخدم أو عنصر واجهة ونموذج بيانات متزامنَين دائمًا، وأي جانب مصدر شرعي للتغيير. أمثلة: حقل نص وخاصية في نموذج العرض؛ زر تبديل وإعداد منطقي.
نمط عملي: نموذج بنمط View-Model
إليك كيف يعمل كلا وضعَي الربط معًا في نموذج بأسلوب MVVM الحقيقي. نموذج العرض يمتلك البيانات؛ العرض يتصل به بالروابط فقط — لا يُكتب أي معالج أحداث في فئة العرض:
import javafx.beans.property.*;
/** نموذج العرض: يمتلك البيانات وقواعد العمل. */
public class PersonViewModel {
private final StringProperty firstName = new SimpleStringProperty("");
private final StringProperty lastName = new SimpleStringProperty("");
// خاصية مشتقة للقراءة فقط: الاسم الكامل محسوب من خاصيتَي المصدر
private final StringProperty fullName = new SimpleStringProperty();
public PersonViewModel() {
// ربط محسوب أحادي الاتجاه (يُغطى بالكامل في الدرس الثالث)
fullName.bind(firstName.concat(" ").concat(lastName));
}
public StringProperty firstNameProperty() { return firstName; }
public StringProperty lastNameProperty() { return lastName; }
public StringProperty fullNameProperty() { return fullName; }
}
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class PersonFormApp extends Application {
@Override
public void start(Stage stage) {
PersonViewModel vm = new PersonViewModel();
TextField firstField = new TextField();
TextField lastField = new TextField();
Label fullLabel = new Label();
// ثنائي الاتجاه: الحقل أو النموذج قادر على بدء التغيير
firstField.textProperty().bindBidirectional(vm.firstNameProperty());
lastField.textProperty().bindBidirectional(vm.lastNameProperty());
// أحادي الاتجاه: fullLabel للقراءة فقط، مشتق دائمًا من النموذج
fullLabel.textProperty().bind(vm.fullNameProperty());
VBox root = new VBox(8,
new Label("الاسم الأول:"), firstField,
new Label("اسم العائلة:"), lastField,
new Label("الاسم الكامل:"), fullLabel
);
root.setPadding(new javafx.geometry.Insets(16));
stage.setScene(new Scene(root, 280, 220));
stage.setTitle("نموذج MVVM");
stage.show();
}
}
اكتب في أي من حقلَي النص وسيتحدث تسمية الاسم الكامل فورًا. استدع vm.firstNameProperty().set("Alice") من أي خيط أو معالج زر وسيتحدث كل من الحقل والتسمية تلقائيًا. هذه هي القيمة الحقيقية لنظام خصائص JavaFX: تدفق بيانات تعريفي دون أي كود مزامنة يدوي.
الخلاصة
تُنشئ bind() تبعية أحادية الاتجاه: تعكس الخاصية المرتبطة مصدرها دائمًا ولا يمكن ضبطها مباشرةً. أما bindBidirectional() فتربط خاصيتَين بحيث تنتشر التغييرات من أي جانب إلى الآخر. ألغِ الربط دائمًا قبل إعادة الاتصال بمصدر جديد، خاصةً في سيناريوهات الربط ثنائية الاتجاه، لتجنب تسريبات الذاكرة. في الدرس القادم ستتجاوز الروابط البسيطة من النوع نفسه وستتعلم كيفية إنشاء روابط محسوبة ومرنة تحوّل القيم وتجمعها وتشتقها عبر أنواع خصائص مختلفة.