MewUI's data binding system uses a delegate-based, reflection-free approach compatible with Native AOT.
Unlike WPF/WinUI, MewUI does not use Reflection:
| WPF Approach | MewUI Approach |
|---|---|
{Binding PropertyName} |
.BindText(vm.Name) or .Bind(property, source) |
INotifyPropertyChanged |
ObservableValue<T> |
| PropertyPath strings | Direct property references |
Benefits:
- Native AOT Compatible: safe for trimming/AOT
- Compile-time Validation: prevents property name typos
- IntelliSense Support: auto-completion available
- Refactoring Safe: automatically reflects renames
public enum BindingMode
{
OneWay, // Source → Control only
TwoWay, // Source ↔ Control (bidirectional)
}The default mode is determined by the property: input properties (e.g., TextBox.TextProperty) default to TwoWay, display properties (e.g., Label.TextProperty) default to OneWay.
A reactive value container that automatically updates the UI when the value changes.
var name = new ObservableValue<string>("Default");
var count = new ObservableValue<int>(0);
var isEnabled = new ObservableValue<bool>(true);
// Read/Write
string current = name.Value;
name.Value = "New Value";
// Change notification
name.Changed += () => Console.WriteLine("Name changed!");var percent = new ObservableValue<double>(50, v => Math.Clamp(v, 0, 100));
percent.Value = 150; // → 100
percent.Value = -10; // → 0
var text = new ObservableValue<string>("", v => v?.Trim() ?? "");MewUI provides three levels of binding:
High-level, per-control convenience methods for common properties.
var name = new ObservableValue<string>("");
var count = new ObservableValue<int>(0);
var isChecked = new ObservableValue<bool>(false);
// Text binding (two-way for TextBox, one-way for Label)
new TextBox().BindText(name)
new Label().BindText(name)
// Conversion binding
new Label().BindText(count, c => $"Count: {c}")
// CheckBox / ToggleSwitch
new CheckBox().BindIsChecked(isChecked)
// Slider / ProgressBar
new Slider().BindValue(volume)
// Visibility / Enabled
new Button().BindIsVisible(isVisible).BindIsEnabled(isEnabled)Binds any MewProperty<T> to an ObservableValue<T>. Works on any MewObject.
// Direct type binding
element.Bind(Control.BackgroundProperty, colorSource)
// With conversion
element.Bind(Control.BackgroundProperty, temperatureSource,
convert: temp => temp > 30 ? Color.Red : Color.Blue)
// With two-way conversion
textBox.Bind(TextBase.TextProperty, intSource,
convert: i => i.ToString(),
convertBack: s => int.TryParse(s, out var v) ? v : 0)The underlying API that fluent methods call. Use for custom controls or advanced scenarios.
// ObservableValue binding
element.SetBinding(property, source, mode: BindingMode.TwoWay);
// With conversion
element.SetBinding(property, source, convert, convertBack, mode);
// MewObject-to-MewObject property binding
// Binds a property on this object to a property on another MewObject.
// Updates at the style (target) tier — local values still take precedence.
element.SetBinding(TextBlock.TextProperty, otherElement, Window.TitleProperty);| Method | Direction | Description |
|---|---|---|
BindText(ObservableValue<string>) |
One-Way | Text binding |
BindText<T>(ObservableValue<T>, Func<T, string>) |
One-Way | Conversion binding |
| Method | Direction | Description |
|---|---|---|
BindText(ObservableValue<string>) |
Two-Way | Text input binding |
| Method | Direction | Description |
|---|---|---|
BindContent(ObservableValue<string>) |
One-Way | Button text binding |
BindContent<T>(ObservableValue<T>, Func<T, string>) |
One-Way | Conversion binding |
| Method | Direction | Description |
|---|---|---|
BindIsChecked(ObservableValue<bool>) |
Two-Way | Checked state binding |
| Method | Direction | Description |
|---|---|---|
BindSelectedIndex(ObservableValue<int>) |
Two-Way | Selection index binding |
| Method | Direction | Description |
|---|---|---|
BindValue(ObservableValue<double>) |
Two-Way | Value binding |
| Method | Direction | Description |
|---|---|---|
BindValue(ObservableValue<double>) |
One-Way | Progress value binding |
| Method | Direction | Description |
|---|---|---|
BindIsVisible(ObservableValue<bool>) |
One-Way | Visibility binding |
BindIsEnabled(ObservableValue<bool>) |
One-Way | Enabled state binding |
| Method | Direction | Description |
|---|---|---|
Bind<TElement, T>(MewProperty<T>, ObservableValue<T>) |
Default | Direct property binding |
Bind<TElement, TProp, TSource>(MewProperty<TProp>, ObservableValue<TSource>, convert, convertBack?) |
Default | Conversion property binding |
class LoginViewModel
{
public ObservableValue<string> Username { get; } = new("");
public ObservableValue<string> Password { get; } = new("");
public ObservableValue<bool> RememberMe { get; } = new(false);
public ObservableValue<string> ErrorMessage { get; } = new("");
public ObservableValue<bool> IsLoading { get; } = new(false);
public void Login()
{
if (string.IsNullOrEmpty(Username.Value))
{
ErrorMessage.Value = "Username is required";
return;
}
IsLoading.Value = true;
// ... login logic
}
}var vm = new LoginViewModel();
new StackPanel()
.Vertical()
.Spacing(8)
.Children(
new TextBox()
.Placeholder("Username")
.BindText(vm.Username),
new TextBox()
.Placeholder("Password")
.BindText(vm.Password),
new CheckBox()
.Content("Remember me")
.BindIsChecked(vm.RememberMe),
new Label()
.Foreground(Color.FromRgb(200, 60, 60))
.BindText(vm.ErrorMessage),
new Button()
.Content("Login")
.OnCanClick(() => !vm.IsLoading.Value)
.OnClick(() => vm.Login())
)Combine multiple ObservableValues to create derived values:
var firstName = new ObservableValue<string>("");
var lastName = new ObservableValue<string>("");
new Label()
.Apply(label =>
{
void Update() => label.Text = $"{firstName.Value} {lastName.Value}".Trim();
firstName.Changed += Update;
lastName.Changed += Update;
Update();
})public static Label BindFullName(this Label label,
ObservableValue<string> firstName,
ObservableValue<string> lastName)
{
void Update() => label.Text = $"{firstName.Value} {lastName.Value}".Trim();
firstName.Changed += Update;
lastName.Changed += Update;
Update();
return label;
}
new Label().BindFullName(vm.FirstName, vm.LastName)Bindings are automatically cleaned up when controls are disposed (e.g., when the Window closes):
var textBox = new TextBox().BindText(vm.Name);
// Binding auto-unsubscribed on disposalvar counter = new ObservableValue<int>(0);
void OnChanged() => Console.WriteLine(counter.Value);
counter.Subscribe(OnChanged);
counter.Unsubscribe(OnChanged); // manual unsubscribe// Good — bindable
class ViewModel
{
public ObservableValue<string> Name { get; } = new("");
}
// Bad — not bindable
class ViewModel
{
public string Name { get; set; }
}var age = new ObservableValue<int>(0, v => Math.Clamp(v, 0, 150));// Good — conversion at binding
new Label().BindText(vm.Price, p => $"${p:N0}")
// Bad — formatting in ViewModel
class ViewModel { public ObservableValue<string> FormattedPrice { get; } }// Fluent shorthand for common properties
new TextBox().BindText(vm.Name)
// Generic Bind for any MewProperty
new Border().Bind(Control.BackgroundProperty, vm.StatusColor)