Skip to content
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

Closed
dim-37 opened this issue Apr 28, 2021 · 33 comments
Labels
area-Binding bug Something isn't working

Comments

@dim-37
Copy link

dim-37 commented Apr 28, 2021

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.

  1. Lunch UWP app on x64
    1
  2. Click on 'To test page' button
    2
  3. Click on 'Reset scope' button
    3
  4. Because the new instances of the main page is created on back stack navigation in frame new bindings propagate new values
    4
  5. Click on 'Reset scope' button
    5
  6. Notice that the number didn’t change because bindings didn’t update
    6
  7. Test Page is bound to view model NavigationView TogglePaneTopPadding issue #2 but the main page updated view model Programmatically adding new NavigationViewItems to an existing NavigationView.MenuItems collection results in the added items rendering inside a generated NavigationViewItem node #1. So sub view model of the test page doesn’t exist in view model NavigationView TogglePaneTopPadding issue #2 hence test page is empty.
    7

Windows app type:

UWP Win32
Yes
Windows 10 version Saw the problem?
Insider Build (xxxxx) Not Tested
October 2020 Update (19042) Yes
May 2020 Update (19041) Not Tested
November 2019 Update (18363) Not Tested
May 2019 Update (18362) Not Tested
October 2018 Update (17763) Not Tested
April 2018 Update (17134) Not Tested
Fall Creators Update (16299) Not Tested
Creators Update (15063) Not Tested
Device form factor Saw the problem?
Desktop Yes
Xbox Not Tested
Surface Hub Not Tested
IoT Not Tested

Additional context
ErrorReproducing.zip

@ghost ghost added the needs-triage Issue needs to be triaged by the area owners label Apr 28, 2021
@ranjeshj
Copy link
Contributor

@dim-37 could you provide a simplified repro with just a uwp app ? I'm unable to load and build the attached project.

@xperiandri
Copy link

@ranjeshj you can remove or unload all the projects except all F#, Elmish, Shared, and UWP

@dim-37
Copy link
Author

dim-37 commented Apr 29, 2021

@ranjeshj Here is the corrected version
How to run :

  1. Open ErrorReproducing\ErrorReproducing.slnf
  2. Build Elmish.Uno.Uwp.
  3. Run ErrorReproducing.UWP
    ErrorReproducing.zip

@xperiandri
Copy link

@ranjeshj were you able to reproduce the issue?

@chrisglein
Copy link
Member

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++).

@chrisglein chrisglein added bug Something isn't working needs-author-feedback Asked author to supply more information. and removed needs-triage Issue needs to be triaged by the area owners product-winui3 WinUI 3 issues labels May 17, 2021
@xperiandri
Copy link

The issue is not about F#.
But about interop of .NET and WinRT

@xperiandri
Copy link

xperiandri commented May 18, 2021

@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 ICustomPropertyProvider in C# as I did in ErrorReproducing\Elmish.Uno\src\Elmish.Uno.Uwp\ViewModel.cs
Just grab my code, remove the base class and put some internal item from which you will return some data.

@xperiandri
Copy link

WinUI 3 perfectly works with F#. I tested. Modern CS WinRT bindings in .NET 5 support any language

@xperiandri
Copy link

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.

@ghost ghost added the no-recent-activity label May 25, 2021
@ghost
Copy link

ghost commented May 25, 2021

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.

@xperiandri
Copy link

@chrisglein @ranjeshj

@ghost ghost removed the no-recent-activity label May 25, 2021
@chrisglein chrisglein added needs-triage Issue needs to be triaged by the area owners and removed needs-author-feedback Asked author to supply more information. labels May 26, 2021
@chiaramooney
Copy link

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".

@xperiandri
Copy link

@chiaramooney can we have your screen sharing session in Microsoft Teams?

@xperiandri
Copy link

@chiaramooney

@xperiandri
Copy link

Any updates?

@xperiandri
Copy link

This now does not work at all in WinUI 0.5.7
Branch https://github.com/xperiandri/Elmish.Uno/tree/uno_winui
ICustomProperty methods are never invoked

@runceel
Copy link

runceel commented Jun 29, 2021

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

repro

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();

@runceel
Copy link

runceel commented Jun 29, 2021

I did try and error to implement ICustomPropertyProvider interface correctly.
I noticed that GetValue method of ICustomProperty has object target argument, and I thought instances of ICustomProperty were cached by UWP data binding engine.

So, I edited the repro project to use the target argument to get property value.
It worked fine.

worked

The project is on correctImplementation branch of the repository.
https://github.com/runceel/CustomPropertyProviderApp/tree/correctImplementation

@xperiandri Could you try that on your project?

@xperiandri
Copy link

Ah! So this is not a bug but a feature and a lack of proper documentation 😁.
Your new sample works.

I try to fix my Elmish.Uno accordingly. I will post an update after finish

@xperiandri
Copy link

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.
Screenshot 6 now works correctly.
But screenshot 7 does not work correctly.
If I press Reset scope on the MainPage number changes, but if I go to the TestPage subviewmodel binds correctly but its properties do not. So that their bindings fail.

@xperiandri
Copy link

This now does not work at all in WinUI 0.5.7
Branch https://github.com/xperiandri/Elmish.Uno/tree/uno_winui
ICustomProperty methods are never invoked

Upgraded to Reunion 0.8.0
Still does not work at all.

@runceel
Copy link

runceel commented Jun 30, 2021

@xperiandri
I might the bug of screenshot 7 is not UWP(WinUI) bugs. Because SetState delegate was called with an object instance of iteration 0 even if I pressed Reset scope button on MainPage. Please put a break point to SetState inner method of MakeSetState method on ViewModel class. And then please do the steps for repro.

You will see the following image after clicking Reset scope button and To test page button. iteration 1 is displayed on MainPage, however lastModel variable is iteration **0**.
image

I was not able to find the root cause of this bug, because I don't have enough knowledge for F#.

@xperiandri
Copy link

@runceel thanks, I will have a look

@xperiandri
Copy link

What about WinUI 3 bug? Do I need to post a new issue about that?

@runceel
Copy link

runceel commented Jun 30, 2021

@xperiandri Sorry, I'm not a contributor of this repository. So, I can't advice for this.

@chiaramooney any suggestion?

The bug is:

  • ICustomPropertyProvider doesn't work on Windows App SDK(Project Reunion) 0.8.
    • The sample code is working on UWP, but that doesn't work on Windows App SDK 0.8.

This is in my understanding. @xperiandri, If you have any additional information, then please add comments.

@xperiandri
Copy link

Correct. Worked on 0.5.6 or 0.5.5

@xperiandri
Copy link

@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.
Even if I call

element.DataContext = null; // if add this, nothing changes
element.DataContext = viewModel;

it makes no effect.

I suppose that is a bug in WinUI binding implementation

@xperiandri
Copy link

image

@runceel
Copy link

runceel commented Jul 8, 2021

@xperiandri
I think it is NOT a bug of the UWP binding engine.
I have added a command property to the model and ViewModel class that is implemented ICustomPropertyProvider interface on addCommand branch on my repository.

https://github.com/runceel/CustomPropertyProviderApp/tree/addCommand

The command worked as well when I replaced DataContext property with a new instance.
commands

Did you implement INotifyPropertyChanged interface correctly on your ViewModel class? Please check my repro project.

@xperiandri
Copy link

xperiandri commented Jul 8, 2021

@runceel thanks.
Fixed

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();
}

@xperiandri
Copy link

xperiandri commented Jul 8, 2021

The issue was present because I've been getting bindings from the first view model, so that command instance was extracted from old bindings

@xperiandri
Copy link

You can close the issue

@xperiandri
Copy link

Created separate issue for WinUI 3

@ghost ghost removed the needs-triage Issue needs to be triaged by the area owners label Jul 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Binding bug Something isn't working
Projects
None yet
Development

No branches or pull requests

7 participants