diff --git a/MainView.dfm b/MainView.dfm index e3eab42..c3f4c13 100644 --- a/MainView.dfm +++ b/MainView.dfm @@ -2,8 +2,8 @@ object MainViewForm: TMainViewForm Left = 0 Top = 0 Caption = 'SimpleMVVMDemo' - ClientHeight = 289 - ClientWidth = 561 + ClientHeight = 373 + ClientWidth = 601 Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText @@ -17,8 +17,8 @@ object MainViewForm: TMainViewForm object GroupBox1: TGroupBox Left = 8 Top = 8 - Width = 161 - Height = 121 + Width = 185 + Height = 153 Caption = 'Example 1' TabOrder = 0 object lblFullName: TLabel @@ -46,10 +46,10 @@ object MainViewForm: TMainViewForm end end object GroupBox2: TGroupBox - Left = 184 + Left = 208 Top = 8 - Width = 169 - Height = 169 + Width = 185 + Height = 153 Caption = 'Example 2' TabOrder = 1 object lblClickCount: TLabel @@ -84,10 +84,10 @@ object MainViewForm: TMainViewForm end end object GroupBox3: TGroupBox - Left = 368 + Left = 408 Top = 8 Width = 185 - Height = 169 + Height = 153 Caption = 'Example 3' TabOrder = 2 object lblPrice: TLabel @@ -114,4 +114,20 @@ object MainViewForm: TMainViewForm TabOrder = 1 end end + object GroupBox4: TGroupBox + Left = 8 + Top = 176 + Width = 185 + Height = 161 + Caption = 'Example 4' + TabOrder = 3 + object cbAvailableCountries: TComboBox + Left = 16 + Top = 24 + Width = 145 + Height = 21 + Style = csDropDownList + TabOrder = 0 + end + end end diff --git a/MainView.pas b/MainView.pas index 92b9348..abbcd22 100644 --- a/MainView.pas +++ b/MainView.pas @@ -41,6 +41,12 @@ TMainViewForm = class(TForm) [Bind('Enabled', 'ChosenTicket')] btnClear: TButton; + GroupBox4: TGroupBox; + [Bind('Value', 'Country')] + [BindOptions('AvailableCountries')] + [BindOptionsCaption('Choose...')] + cbAvailableCountries: TComboBox; + procedure FormCreate(Sender: TObject); end; diff --git a/MainViewModel.pas b/MainViewModel.pas index 0172212..9fde9e2 100644 --- a/MainViewModel.pas +++ b/MainViewModel.pas @@ -27,6 +27,8 @@ TViewModel = class(TComponent) fHasClickedTooManyTimes: IObservable; fChosenTicket: IObservable; fTickets: TList; + fAvailableCountries: TArray; + fCountry: IObservable; function GetLastName: string; procedure SetLastName(const value: string); @@ -48,6 +50,9 @@ TViewModel = class(TComponent) property ChosenTicket: IObservable read fChosenTicket; property Tickets: TList read fTickets; + + property AvailableCountries: TArray read fAvailableCountries; + property Country: IObservable read fCountry; end; implementation @@ -76,7 +81,7 @@ constructor TViewModel.Create(const firstName, lastName: string); end); // Example 2 - fNumberOfClicks := TObservable.Create(0); + fNumberOfClicks := TObservable.Create; fHasClickedTooManyTimes := TDependentObservable.Create( function: Boolean begin @@ -84,12 +89,16 @@ constructor TViewModel.Create(const firstName, lastName: string); end); // Example 3 - fChosenTicket := TObservable.Create(nil); - fTickets := TObjectList.Create(); + fChosenTicket := TObservable.Create; + fTickets := TObjectList.Create; fTickets.AddRange([ TTicket.Create('Economy', 199.95), TTicket.Create('Business', 449.22), TTicket.Create('First Class', 1199.99)]); + + // Example 4 + fAvailableCountries := TArray.Create('AU', 'NZ', 'US'); + fCountry := TObservable.Create; end; destructor TViewModel.Destroy; diff --git a/SimpleMVVM.Binding.Components.pas b/SimpleMVVM.Binding.Components.pas index ae18d6b..5823933 100644 --- a/SimpleMVVM.Binding.Components.pas +++ b/SimpleMVVM.Binding.Components.pas @@ -136,7 +136,7 @@ constructor TBinding.Create(const component: T; const observable: IObservable begin inherited Create(component); fComponent := component; - fObservable := TDependentObservable.Create( + fObservable := TDependentObservable.Create( Initialize(observable), procedure(const value: TValue) begin @@ -246,11 +246,19 @@ function TEditBinding.Initialize(const observable: IObservable): TFunc; {$REGION 'TComboBoxBinding'} procedure TComboBoxBinding.HandleChange(Sender: TObject); +var + o: TObject; begin if fComponent.ItemIndex = -1 then fObservable.Value := nil else - fObservable.Value := fComponent.Items.Objects[fComponent.ItemIndex]; + begin + o := fComponent.Items.Objects[fComponent.ItemIndex]; + if o = nil then + fObservable.Value := fComponent.Items[fComponent.ItemIndex] + else + fObservable.Value := o; + end; end; procedure TComboBoxBinding.InitComponent; @@ -262,8 +270,14 @@ function TComboBoxBinding.Initialize(const observable: IObservable): TFunc, + for field in ctx.GetType(view.ClassInfo).GetFields do + for attr in field.GetAttributes do + if attr is BindAttribute then + BindAttribute(attr).ApplyBinding( + field.GetValue(view).AsType, viewModel); end; -{$REGION 'BindAttribute'} +function CreateObservable(const instance: TObject; + const expression: string): IObservable; -type - TPropertyObservable = class(TObservableBase) - private - fProperty: TRttiProperty; - fInstance: TObject; - function GetValueNonGeneric: TValue; override; - procedure SetValueNonGeneric(const value: TValue); override; - public - constructor Create(const prop: TRttiProperty; const instance: TObject); + function CreateRootProp(const prop: TRttiProperty; const instance: TObject): IObservable; + begin + Result := TDependentObservable.Create( + function: TValue + begin + Result := prop.GetValue(instance); + end, + procedure(const value: TValue) + begin + prop.SetValue(instance, value); + end); end; - TDependentObservable = class(TObservableBase) - private - fProperty: TRttiProperty; - fObservable: IObservable; - function GetValueNonGeneric: TValue; override; - procedure SetValueNonGeneric(const value: TValue); override; - public - constructor Create(const prop: TRttiProperty; const observable: IObservable); + function CreateSubProp(const prop: TRttiProperty; const observable: IObservable): IObservable; + begin + Result := TDependentObservable.Create( + function: TValue + var + instance: TObject; + begin + instance := observable.Value.AsObject; + if Assigned(instance) then + Result := prop.GetValue(instance) + else + Result := nil; + end, + procedure(const value: TValue) + var + instance: TObject; + begin + instance := observable.Value.AsObject; + if Assigned(instance) then + prop.SetValue(instance, value); + end); end; -{ TPropertyObservable } - -constructor TPropertyObservable.Create(const prop: TRttiProperty; - const instance: TObject); -begin - inherited Create; - fProperty := prop; - fInstance := instance; -end; - -function TPropertyObservable.GetValueNonGeneric: TValue; +var + expressions: TStringDynArray; + i: Integer; + typ: TRttiType; + prop: TRttiProperty; begin - RegisterDependency; - - ObservableStack.Push(Self); - try - Result := fProperty.GetValue(fInstance); - finally - ObservableStack.Pop; + Result := nil; + expressions := SplitString(expression, '.'); + typ := ctx.GetType(instance.ClassInfo); + for i := 0 to High(expressions) do + begin + prop := typ.GetProperty(expressions[i]); + if Assigned(prop) then + if StartsText('IObservable<', prop.PropertyType.Name) then + begin + Result := prop.GetValue(instance).AsInterface as IObservable; + typ := prop.PropertyType.GetMethod('GetValue').ReturnType; + end + else + begin + if i = 0 then + Result := CreateRootProp(prop, instance) + else + Result := CreateSubProp(prop, Result); + typ := prop.PropertyType; + end; end; end; -procedure TPropertyObservable.SetValueNonGeneric(const value: TValue); +procedure Bind(const target: TComponent; const targetExpression: string; + const source: TObject; const sourceExpression: string); +var + observable: IObservable; + typ: TRttiType; + method: TRttiMethod; + action: ICommand; begin - fProperty.SetValue(fInstance, value); -end; + observable := CreateObservable(source, sourceExpression); -{ TDependentObservable } + if not Assigned(observable) then + begin + typ := ctx.GetType(source.ClassInfo); + method := typ.GetMethod(sourceExpression); + if Assigned(method) then + action := TCommand.Create(method, source); + end; -constructor TDependentObservable.Create(const prop: TRttiProperty; - const observable: IObservable); -begin - inherited Create; - fProperty := prop; - fObservable := observable; + // hardcode for now, build better rules later + if (target is TEdit) and SameText(targetExpression, 'Value') then + TEditBinding.Create(TEdit(target), observable) + else if (target is TComboBox) and SameText(targetExpression, 'Value') then + TComboBoxBinding.Create(TComboBox(target), observable) + else if (target is TLabel) and SameText(targetExpression, 'Text') then + TLabelBinding.Create(TLabel(target), observable) + else if (target is TButton) and SameText(targetExpression, 'Click') then + TButtonBinding.Create(TButton(target), action) + else + TComponentBinding.Create(target, observable, targetExpression); end; -function TDependentObservable.GetValueNonGeneric: TValue; -var - v: TValue; - obj: TObject; -begin - RegisterDependency; - - ObservableStack.Push(Self); - try - v := fObservable.Value; - obj := v.AsObject; - if Assigned(obj) then - Result := fProperty.GetValue(obj) - else - Result := nil; - finally - ObservableStack.Pop; - end; -end; -procedure TDependentObservable.SetValueNonGeneric(const value: TValue); -var - v: TValue; - obj: TObject; -begin - v := fObservable.Value; - obj := v.AsObject; - if Assigned(obj) then - fProperty.SetValue(obj, value); -end; +{$REGION 'BindAttribute'} constructor BindAttribute.Create(const targetName, sourceName: string); begin @@ -179,55 +190,8 @@ constructor BindAttribute.Create(const targetName, sourceName: string); procedure BindAttribute.ApplyBinding(const target: TComponent; const source: TObject); -var - t: TRttiType; - p: TRttiProperty; - m: TRttiMethod; - observable: IObservable; - action: ICommand; - sourceName: TStringDynArray; - i: Integer; begin - t := ctx.GetType(source.ClassInfo); - // TODO: extract to extra method to generate chained expressions - sourceName := SplitString(fSourceName, '.'); - - for i := 0 to High(sourceName) do - begin - p := t.GetProperty(sourceName[i]); - if Assigned(p) then - begin - if StartsText('IObservable<', p.PropertyType.Name) then - begin - observable := p.GetValue(source).AsInterface as IObservable; - t := p.PropertyType.GetMethod('GetValue').ReturnType; - end - else - begin - if i = 0 then - observable := TPropertyObservable.Create(p, source) - else - observable := TDependentObservable.Create(p, observable); - t := p.PropertyType; - end; - end; - end; - - m := t.GetMethod(sourceName[0]); - if Assigned(m) then - action := TCommand.Create(m, source); - - // hardcode for now, build better rules later - if (target is TEdit) and SameText(fTargetName, 'Value') then - TEditBinding.Create(TEdit(target), observable) - else if (target is TComboBox) and SameText(fTargetName, 'Value') then - TComboBoxBinding.Create(TComboBox(target), observable) - else if (target is TLabel) and SameText(fTargetName, 'Text') then - TLabelBinding.Create(TLabel(target), observable) - else if (target is TButton) and SameText(fTargetName, 'Click') then - TButtonBinding.Create(TButton(target), action) - else - TComponentBinding.Create(target, observable, fTargetName); + Bind(target, fTargetName, source, fSourceName); end; {$ENDREGION} @@ -246,10 +210,11 @@ procedure BindOptionsAttribute.ApplyBinding(const target: TComponent; a: TCustomAttribute; t: TRttiType; p: TRttiProperty; - s: TStrings; + items: TStrings; l: TList; o: TObject; i: Integer; + s: string; optionsCaption: string; optionsText: string; begin @@ -264,18 +229,24 @@ procedure BindOptionsAttribute.ApplyBinding(const target: TComponent; t := ctx.GetType(source.ClassInfo); p := t.GetProperty(fSourceName); if Assigned(p) then - if StartsText('TList<', p.PropertyType.Name) then + // just hardcode this for the Items property for now, make it dynamic later + if target is TComboBox then begin - // just hardcode this for the Items property for now, make it dynamic later - if target is TComboBox then + items := TComboBox(target).Items; + items.Clear; + if optionsCaption <> '' then + begin + items.Add(optionsCaption); + TComboBox(target).ItemIndex := 0; + end; + + if p.PropertyType.Handle = TypeInfo(TArray) then + begin + for s in p.GetValue(source).AsType> do + items.Add(s); + end else + if StartsText('TList<', p.PropertyType.Name) then begin - s := TComboBox(target).Items; - s.Clear; - if optionsCaption <> '' then - begin - s.Add(optionsCaption); - TComboBox(target).ItemIndex := 0; - end; // assume that it is a list of objects for now l := TList(p.GetValue(source).AsObject); for i := 0 to l.Count - 1 do @@ -284,11 +255,10 @@ procedure BindOptionsAttribute.ApplyBinding(const target: TComponent; if optionsText <> '' then p := ctx.GetType(o.ClassInfo).GetProperty(optionsText); if Assigned(p) then - s.AddObject(p.GetValue(o).ToString, o) + items.AddObject(p.GetValue(o).ToString, o) else - s.AddObject(o.ToString, o); + items.AddObject(o.ToString, o); end; - end; end; end; diff --git a/SimpleMVVM.Observable.pas b/SimpleMVVM.Observable.pas index 5232986..59f2609 100644 --- a/SimpleMVVM.Observable.pas +++ b/SimpleMVVM.Observable.pas @@ -30,10 +30,12 @@ interface property Value: T read GetValue write SetValue; end; + TAction = reference to procedure (const Arg1: T); + TObservableBase = class(TInterfacedObject, IObservable) private fDependencies: TList; - protected + strict protected class var ObservableStack: TStack; constructor Create; procedure Changed; virtual; @@ -49,6 +51,22 @@ TObservableBase = class(TInterfacedObject, IObservable) destructor Destroy; override; end; + TDependentObservable = class(TObservableBase) + protected + fValue: TValue; + fGetter: TFunc; + fSetter: TAction; + fIsNotifying: Boolean; + fNeedsEvaluation: Boolean; + procedure Changed; override; + procedure Evaluate; + function GetValueNonGeneric: TValue; override; final; + procedure SetValueNonGeneric(const value: TValue); override; final; + public + constructor Create(const getter: TFunc); overload; + constructor Create(const getter: TFunc; const setter: TAction); overload; + end; + TObservable = class(TObservableBase, IObservable) private fValue: T; @@ -65,8 +83,6 @@ TObservable = class(TObservableBase, IObservable) property Value: T read GetValue write SetValue; end; - TAction = reference to procedure (const Arg1: T); - TDependentObservable = class(TObservableBase, IObservable) private fValue: T; @@ -136,6 +152,61 @@ procedure TObservableBase.RegisterDependency; {$ENDREGION} +{$REGION 'TDependentObservable'} + +constructor TDependentObservable.Create(const getter: TFunc); +begin + Create(getter, nil); +end; + +constructor TDependentObservable.Create(const getter: TFunc; + const setter: TAction); +begin + inherited Create; + fGetter := getter; + fSetter := setter; + fNeedsEvaluation := True; + Evaluate; +end; + +procedure TDependentObservable.Changed; +begin + Evaluate; + inherited; +end; + +procedure TDependentObservable.Evaluate; +begin + if fIsNotifying then Exit; + fIsNotifying := True; + RegisterDependency; + + ObservableStack.Push(Self); + try + fValue := fGetter; + finally + ObservableStack.Pop; + fIsNotifying := False; + fNeedsEvaluation := False; + end; +end; + +function TDependentObservable.GetValueNonGeneric: TValue; +begin + if fNeedsEvaluation or (ObservableStack.Count > 0) then + Evaluate; + Result := fValue; +end; + +procedure TDependentObservable.SetValueNonGeneric(const value: TValue); +begin + fSetter(value); + inherited Changed; +end; + +{$ENDREGION} + + {$REGION 'TObservable'} class constructor TObservable.Create; @@ -178,7 +249,6 @@ procedure TObservable.SetValueNonGeneric(const value: TValue); begin SetValue(value.AsType); end; - {$ENDREGION} @@ -229,18 +299,8 @@ function TDependentObservable.GetValue: T; end; function TDependentObservable.GetValueNonGeneric: TValue; -type - PValue = ^TValue; -var - v: T; begin - if TypeInfo(T) = TypeInfo(TValue) then - begin - v := GetValue; - Result := PValue(@v)^; - end - else - Result := TValue.From(GetValue); + Result := TValue.From(GetValue); end; procedure TDependentObservable.SetValue(const value: T); @@ -250,18 +310,8 @@ procedure TDependentObservable.SetValue(const value: T); end; procedure TDependentObservable.SetValueNonGeneric(const value: TValue); -type - PValue = ^TValue; -var - v: T; begin - if TypeInfo(T) = TypeInfo(TValue) then - begin - PValue(@v)^ := value; - SetValue(v); - end - else - SetValue(value.AsType); + SetValue(value.AsType); end; {$ENDREGION}