-
Notifications
You must be signed in to change notification settings - Fork 697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Swapping view model (same type, new instance) that exposes ICustomPropertyProvider does not trigger Bindings update #4913
Comments
@dim-37 could you provide a simplified repro with just a uwp app ? I'm unable to load and build the attached project. |
@ranjeshj you can remove or unload all the projects except all F#, Elmish, Shared, and UWP |
@ranjeshj Here is the corrected version
|
@ranjeshj were you able to reproduce the issue? |
Looking at the project this appears to be system XAML (UWP), not WinUI3. Clearing that label. @dim-37 There's a lot going on here with F# and Uno. Is that essential to the repro? Sounds like from the above it's okay to unload them? Where's the code in your sample that's setting that instance count? Likely the fix here would need to go in WinUI3, as that's where these sorts of bugs are being fixed. But FYI I don't believe that works with F# (just C# and C++). |
The issue is not about F#. |
@chrisglein the last sample is the simplest and smallest one. If you open the solution with SLNF file you will get only 5 projects loaded. If you want to get rid of F# completely and have a single project you can implement |
WinUI 3 perfectly works with F#. I tested. Modern CS WinRT bindings in .NET 5 support any language |
The bug is somewhere in WinUI implementation. Because the Uno Platform version of WinUI reimplemented in C# works fine, and this bug is not reproduced on Uno. |
This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 7 days. It will be closed if no further activity occurs within 7 days of this comment. |
Just tried the latest zip sent and I'm experiencing loading errors when I open ErrorProducing.sln. None of the projects are able to load into the solution and produced the error "The project file could not be found". |
@chiaramooney can we have your screen sharing session in Microsoft Teams? |
Any updates? |
This now does not work at all in WinUI 0.5.7 |
I have created a minimum project for repro for this issue using C#. I think this project will work on your side, @chiaramooney. https://github.com/runceel/CustomPropertyProviderApp The bottom button is replacing DataContext property of the page to new regular ViewModel class. private void Button_Click2(object sender, RoutedEventArgs e) => DataContext = new MainPageViewModel(); The top button is replacing that to new ViewModel class that is implemented ICustomPropertyProvider interface. The TextBlock was not updated. private void Button_Click(object sender, RoutedEventArgs e) => DataContext = new CustomPropertyProviderMainPageViewModel(); |
I did try and error to implement So, I edited the repro project to use the target argument to get property value. The project is on correctImplementation branch of the repository. @xperiandri Could you try that on your project? |
Ah! So this is not a bug but a feature and a lack of proper documentation 😁. I try to fix my Elmish.Uno accordingly. I will post an update after finish |
So I fixed my implementation according to @runceel's fix using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input;
using Elmish.Uno;
using Microsoft.FSharp.Collections;
using Microsoft.FSharp.Core;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
namespace Elmish.Windows
{
public class DynamicCustomProperty<TTarget, TValue> : ICustomProperty
{
public Func<TTarget, TValue> Getter { get; }
public Action<TTarget, TValue> Setter { get; }
public Func<TTarget, object, TValue> IndexGetter { get; }
public Action<TTarget, object, TValue> IndexSetter { get; }
public object GetValue(object target) => Getter.Invoke((TTarget) target);
public void SetValue(object target, object value) => Setter.Invoke((TTarget)target, (TValue)value);
public object GetIndexedValue(object target, object index) => IndexGetter.Invoke((TTarget)target, index);
public void SetIndexedValue(object target, object value, object index) => IndexSetter.Invoke((TTarget)target, index, (TValue)value);
public bool CanRead => Getter != null || IndexGetter != null;
public bool CanWrite => Setter != null || IndexSetter != null;
public string Name { get; }
public Type Type => typeof(TValue);
public DynamicCustomProperty(string name, Func<TTarget, TValue> getter, Action<TTarget, TValue> setter = null, Func<TTarget, object, TValue> indexGetter = null, Action<TTarget, object, TValue> indexSetter = null)
{
Name = name;
Getter = getter;
Setter = setter;
IndexGetter = indexGetter;
IndexSetter = indexSetter;
}
}
internal class ViewModel<TModel, TMsg> : Uno.ViewModel<TModel, TMsg>, ICustomPropertyProvider
{
public ViewModel(TModel initialModel, FSharpFunc<TMsg, Unit> dispatch, FSharpList<Binding<TModel, TMsg>> bindings, ElmConfig config, string propNameChain) : base(initialModel, dispatch, bindings, config, propNameChain) { }
public override Uno.ViewModel<object, object> Create(object initialModel, FSharpFunc<object, Unit> dispatch, FSharpList<Binding<object, object>> bindings, ElmConfig config, string propNameChain)
=> new ViewModel<object, object>(initialModel, dispatch, bindings, config, propNameChain);
private ICustomProperty GetProperty(string name)
{
if (name == "CurrentModel") return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.CurrentModel);
if (name == "HasErrors") return new DynamicCustomProperty<ViewModel<TModel, TMsg>, bool>(name, vm => ((INotifyDataErrorInfo)vm).HasErrors);
if (!this.Bindings.TryGetValue(name, out var binding)) Debugger.Break();
switch (binding)
{
case VmBinding<TModel, TMsg>.OneWay oneWay:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(oneWay));
case VmBinding<TModel, TMsg>.OneWayLazy oneWayLazy:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(oneWayLazy));
case VmBinding<TModel, TMsg>.OneWaySeq oneWaySeq:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ObservableCollection<object>>(name,
vm => (ObservableCollection<object>)vm.TryGetMember(oneWaySeq));
case VmBinding<TModel, TMsg>.TwoWay twoWay:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(twoWay), (vm, value) => vm.TrySetMember(value, twoWay));
case VmBinding<TModel, TMsg>.TwoWayValidate twoWayValidate:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(twoWayValidate), (vm, value) => vm.TrySetMember(value, twoWayValidate));
case VmBinding<TModel, TMsg>.Cmd cmd:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ICommand>(name, vm => vm.TryGetMember(cmd) as ICommand);
case VmBinding<TModel, TMsg>.CmdParam cmdParam:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(cmdParam));
case VmBinding<TModel, TMsg>.SubModel subModel:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ViewModel<object, object>>(name,
vm => vm.TryGetMember(subModel) as ViewModel<object, object>);
case VmBinding<TModel, TMsg>.SubModelSeq subModelSeq:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ObservableCollection<Uno.ViewModel<object, object>>>(name,
vm => (ObservableCollection<Uno.ViewModel<object, object>>)vm.TryGetMember(subModelSeq));
case VmBinding<TModel, TMsg>.SubModelSelectedItem subModelSelectedItem:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ViewModel<object, object>>(name,
vm => (ViewModel<object, object>) vm.TryGetMember(subModelSelectedItem));
case VmBinding<TModel, TMsg>.Cached cached:
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name,
vm => vm.TryGetMember(cached), (vm, value) => vm.TrySetMember(value, cached));
default:
return null;
//throw new NotSupportedException();
}
}
public ICustomProperty GetCustomProperty(string name) => GetProperty(name);
public ICustomProperty GetIndexedProperty(string name, Type type) => GetProperty(name);
public string GetStringRepresentation() => CurrentModel.ToString();
public Type Type => CurrentModel.GetType();
}
}
namespace Elmish.Uno
{
[RequireQualifiedAccess, CompilationMapping(SourceConstructFlags.Module)]
public static class ViewModel
{
public static object DesignInstance<TModel, TMsg>(TModel model, FSharpList<Binding<TModel, TMsg>> bindings)
{
var emptyDispatch = FuncConvert.FromAction((TMsg msg) => { });
return new Elmish.Windows.ViewModel<TModel, TMsg>(model, emptyDispatch, bindings, ElmConfig.Default, "main");
}
public static object DesignInstance<T, TModel, TMsg>(TModel model, Program<T, TModel, TMsg, FSharpList<Binding<TModel, TMsg>>> program)
{
var emptyDispatch = FuncConvert.FromAction((TMsg msg) => { });
var mapping = FSharpFunc<TModel, FSharpFunc<TMsg, Unit>>.InvokeFast(ProgramModule.view(program), model, emptyDispatch);
return DesignInstance(model, mapping);
}
public static void StartLoop<TModel, TMsg>(ElmConfig config, FrameworkElement element, Action<Program<Microsoft.FSharp.Core.Unit, TModel, TMsg, FSharpList<Binding<TModel, TMsg>>>> programRun, Program<Unit, TModel, TMsg, FSharpList<Binding<TModel, TMsg>>> program)
{
FSharpRef<FSharpOption<ViewModel<TModel, TMsg>>> lastModel = new FSharpRef<FSharpOption<ViewModel<TModel, TMsg>>>(null);
FSharpFunc<FSharpFunc<TMsg, Unit>, FSharpFunc<TMsg, Unit>> syncDispatch =
FuncConvert.FromAction(MakeSyncDispatch<TMsg>(element));
var setSate = FuncConvert.FromAction(MakeSetState(config, element, program, lastModel));
programRun.Invoke(
ProgramModule.withSyncDispatch(syncDispatch,
ProgramModule.withSetState(setSate, program)));
}
public static void StartLoop<T, TModel, TMsg>(ElmConfig config, FrameworkElement element, Action<T, Program<T, TModel, TMsg, FSharpList<Binding<TModel, TMsg>>>> programRun, Program<T, TModel, TMsg, FSharpList<Binding<TModel, TMsg>>> program, T arg)
{
FSharpRef<FSharpOption<ViewModel<TModel, TMsg>>> lastModel = new FSharpRef<FSharpOption<ViewModel<TModel, TMsg>>>(null);
FSharpFunc<FSharpFunc<TMsg, Unit>, FSharpFunc<TMsg, Unit>> syncDispatch =
FuncConvert.FromAction(MakeSyncDispatch<TMsg>(element));
var setSate = FuncConvert.FromAction(MakeSetState(config, element, program, lastModel));
programRun.Invoke(arg,
ProgramModule.withSyncDispatch(syncDispatch,
ProgramModule.withSetState(setSate, program)));
}
private static Action<TModel, FSharpFunc<TMsg, Unit>> MakeSetState<TArg, TModel, TMsg>(ElmConfig config, FrameworkElement element, Program<TArg, TModel, TMsg, FSharpList<Binding<TModel, TMsg>>> program, FSharpRef<FSharpOption<ViewModel<TModel, TMsg>>> lastModel)
{
void SetState(TModel model, FSharpFunc<TMsg, Unit> dispatch)
{
FSharpOption<ViewModel<TModel, TMsg>> contents = lastModel.contents;
if (contents != null)
{
contents.Value.UpdateModel(model);
return;
}
var bindedModel = ProgramModule.view(program).Invoke(model);
var Bindings = bindedModel.Invoke(dispatch);
var viewModel = new Elmish.Windows.ViewModel<TModel, TMsg>(model, dispatch, Bindings, config, "main");
element.DataContext = viewModel;
lastModel.contents = FSharpOption<ViewModel<TModel, TMsg>>.Some(viewModel);
}
return SetState;
}
private static Action<FSharpFunc<TMsg, Unit>, TMsg> MakeSyncDispatch<TMsg>(FrameworkElement element)
{
void UiDispatch(FSharpFunc<TMsg, Unit> innerDispatch, TMsg msg)
{
void DoDispatch(TMsg m)
{
Console.WriteLine("Dispatch");
innerDispatch.Invoke(m);
}
_ = element.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => DoDispatch(msg));
}
return UiDispatch;
}
}
} And it partially fixed my issue. |
Upgraded to Reunion 0.8.0 |
@xperiandri You will see the following image after clicking I was not able to find the root cause of this bug, because I don't have enough knowledge for F#. |
@runceel thanks, I will have a look |
What about WinUI 3 bug? Do I need to post a new issue about that? |
@xperiandri Sorry, I'm not a contributor of this repository. So, I can't advice for this. @chiaramooney any suggestion? The bug is:
This is in my understanding. @xperiandri, If you have any additional information, then please add comments. |
Correct. Worked on 0.5.6 or 0.5.5 |
@runceel so I debugged the code and even though commands from the next view model are retrieved (when I reset scope, new view model is created and set to DataContext) but old commands from previous view model called.
it makes no effect. I suppose that is a bug in WinUI binding implementation |
@xperiandri https://github.com/runceel/CustomPropertyProviderApp/tree/addCommand The command worked as well when I replaced DataContext property with a new instance. Did you implement |
@runceel thanks. internal class ViewModel<TModel, TMsg> : Uno.ViewModel<TModel, TMsg>, ICustomPropertyProvider
{
private readonly ImmutableDictionary<string, string> bindings;
public ViewModel(TModel initialModel, FSharpFunc<TMsg, Unit> dispatch, FSharpList<Binding<TModel, TMsg>> bindings, ElmConfig config, string propNameChain) : base(initialModel, dispatch, bindings, config, propNameChain)
{
string GetBindingType(Binding<TModel, TMsg> binding)
{
//var (info, _) = FSharpValue.GetUnionFields(binding.Data, typeof(BindingData<TModel, TMsg>), FSharpOption<BindingFlags>.Some(BindingFlags.NonPublic));
return binding.Data.GetType().Name;
}
this.bindings = bindings.ToImmutableDictionary(b => b.Name, GetBindingType);
}
public override Uno.ViewModel<object, object> Create(object initialModel, FSharpFunc<object, Unit> dispatch, FSharpList<Binding<object, object>> bindings, ElmConfig config, string propNameChain)
=> new ViewModel<object, object>(initialModel, dispatch, bindings, config, propNameChain);
private ICustomProperty GetProperty(string name)
{
if (name == "CurrentModel") return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.CurrentModel);
if (name == "HasErrors") return new DynamicCustomProperty<ViewModel<TModel, TMsg>, bool>(name, vm => ((INotifyDataErrorInfo)vm).HasErrors);
if (!bindings.TryGetValue(name, out var bindingType)) Debugger.Break();
switch (bindingType)
{
case nameof(BindingData<TModel, TMsg>.OneWayData):
case nameof(BindingData<TModel, TMsg>.OneWayLazyData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(vm.Bindings[name]));
case nameof(BindingData<TModel, TMsg>.OneWaySeqLazyData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ObservableCollection<object>>(name,
vm => (ObservableCollection<object>)vm.TryGetMember(vm.Bindings[name]));
case nameof(BindingData<TModel, TMsg>.TwoWayData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name,
vm => vm.TryGetMember(vm.Bindings[name]), (vm, value) => vm.TrySetMember(value, vm.Bindings[name]));
case nameof(BindingData<TModel, TMsg>.TwoWayValidateData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name,
vm => vm.TryGetMember(vm.Bindings[name]), (vm, value) => vm.TrySetMember(value, vm.Bindings[name]));
case nameof(BindingData<TModel, TMsg>.CmdData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ICommand>(name,
vm => vm.TryGetMember(vm.Bindings[name]) as ICommand);
case nameof(BindingData<TModel, TMsg>.CmdParamData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, object>(name, vm => vm.TryGetMember(vm.Bindings[name]));
case nameof(BindingData<TModel, TMsg>.SubModelData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ViewModel<object, object>>(name,
vm => vm.TryGetMember(vm.Bindings[name]) as ViewModel<object, object>);
case nameof(BindingData<TModel, TMsg>.SubModelSeqData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ObservableCollection<Uno.ViewModel<object, object>>>(name,
vm => (ObservableCollection<Uno.ViewModel<object, object>>)vm.TryGetMember(vm.Bindings[name]));
case nameof(BindingData<TModel, TMsg>.SubModelSelectedItemData):
return new DynamicCustomProperty<ViewModel<TModel, TMsg>, ViewModel<object, object>>(name,
vm => (ViewModel<object, object>)vm.TryGetMember(vm.Bindings[name]));
default:
return null;
//throw new NotSupportedException();
}
}
public ICustomProperty GetCustomProperty(string name) => GetProperty(name);
public ICustomProperty GetIndexedProperty(string name, Type type) => GetProperty(name);
public string GetStringRepresentation() => CurrentModel.ToString();
public Type Type => CurrentModel.GetType();
} |
The issue was present because I've been getting bindings from the first view model, so that command instance was extracted from old bindings |
You can close the issue |
Created separate issue for WinUI 3 |
In the uploaded semple we replace the current page data context with a new instance of the view model.
The number on the screen in a number of view model instances.
Windows app type:
Additional context
ErrorReproducing.zip
The text was updated successfully, but these errors were encountered: