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

[Feature] MVVM Toolkit vNext: source generators! 🚀 #8

Closed
11 tasks done
Sergio0694 opened this issue Mar 22, 2021 · 126 comments
Closed
11 tasks done

[Feature] MVVM Toolkit vNext: source generators! 🚀 #8

Sergio0694 opened this issue Mar 22, 2021 · 126 comments
Assignees
Labels
feature 💡 A new feature being implemented improvements ✨ Improvements to an existing functionality mvvm-toolkit 🧰 Issues/PRs for the MVVM Toolkit open discussion ☎️ An issue open for active community discussion optimization ☄ Performance or memory usage improvements planning 📑 An issue tracking planning around a milestone

Comments

@Sergio0694
Copy link
Member

Sergio0694 commented Mar 22, 2021

The Microsoft.Toolkit.Mvvm package (aka MVVM Toolkit) is finally out, so it's time to start planning for the next update 😄
This issue is meant to be for tracking planned features, discuss them and possibly propose new ideas well (just like in CommunityToolkit/WindowsCommunityToolkit#3428).

One aspect I want to investigate for this next release, which I think makes sense for the package, is support for Roslyn source generators. Support for these would be added through a secondary project, ie. Microsoft.Toolkit.Mvvm.SourceGenerators, that would be shipped together with the MVVM Toolkit, but then added to consuming projects as a development dependency, ie. as an analyzer. They would be included in the same NuGet package, so it wouldn't be possible to just reference the source generator directly (which wouldn't make sense anyway). This is the same setup I'm using in my own library ComputeSharp, in fact I'm applying a lot of the things I learnt while working on that project to the MVVM Toolkit as well 🥳

There are two three main aspects that can be investigated through support for source generators:

  • Better extensibility and composition (opt-in)
  • Less verbosity (opt-in)
  • Better performance (always on)

Here's some more details about what I mean by these two categories exactly.

Better extensibility

There's one "problem" with C# that has been brought up by devs before while using the MVVM Toolkit and in general (eg. here): lack for multiple inheritance. While that makes perfect sense and there's plenty of valid reasons why that's the case, the fact remains that it makes things sometimes inconvenient, when eg. you want/need to inherit from a specific type but then still want to use the features exposed by other classes in the MVVM Toolkit. The solution? Source generators! 🚀

For now I'm thinking about adding the following attributes to the package:

  • [INotifyPropertyChanged]
  • [ObservableObject]
  • [ObservableRecipient]

The way they work is that they let you annotate a type and then rely on the generator injecting all the APIs from those types automatically, so you don't need to worry about it and it's like you were effectively having multiple inheritance. Here's an example:

[ObservableObject]
partial class MyViewModel : SomeOtherClass
{
}

This class now inherits from SomeOtherClass, but it still has all the same APIs from ObservableObject! This includes the PropertyChanged and PropertyChanging events, the methods to raise them and all the additional helper methods!

[ObservableRecipient] does the same but copying members from ObservableRecipient (eg. this could be used to effectively have a type that is both a validator but also a recipient), and [INotifyPropertyChanged] instead offers minimal support just for INotifyPropertyChanged, with optionally also the ability to include additional helpers or not. This approach and the different attributes offer maximum flexibility for users to choose the best way to construct their architecture without having to compromise between what APIs to use from the MVVM Toolkit and how they want/have to organize their type hierarchy. 🎉

NOTE: this category is marked as "opt-in" because the attributes are completely optional. Not using them will have no changes at all on the behavior of the toolkit, so developers just wanting to inherit from the base types in the library as usual will absolutely still be able to do so. This just gives consumers more flexibility depending on their exact use case scenario.

Less verbosity

This was first suggested by @michael-hawker in this comment, the idea is to also provide helpers to reduce the code verbosity in simple cases, such as when defining classic observable properties. For now I've added these attributes:

  • [ObservableProperty]
  • [AlsoNotifyFor]
  • [ICommand]

The first two can be used to easily declare observable properties, by annotating a field. [ObservableProperty] will create the code necessary to implement the property itself, whereas [AlsoNotifyFor] will customize the generated code by adding extra notification events (ie. calls to OnPropertyChanged) for properties whose value depends on the property being updated.

Here's an example of how these two attributes can be used together:

Viewmodel definition:
public sealed partial class PersonViewModel : ObservableObject
{
    [ObservableProperty]
    [AlsoNotifyFor(nameof(FullName))]
    private string name;

    [ObservableProperty]
    [AlsoNotifyFor(nameof(FullName))]
    private string surname;

    public string FullName => $"{Name} {Surname}";
}
Generated code:
public sealed partial class PersonViewModel
{
    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public string Name
    {
        get => name;
        set
        {
            if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(name, value))
            {
                OnPropertyChanging();
                name = value;
                OnPropertyChanged();
                OnPropertyChanged("FullName");
            }
        }
    }

    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public string Surname
    {
        get => surname;
        set
        {
            if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(surname, value))
            {
                OnPropertyChanging();
                surname = value;
                OnPropertyChanged();
                OnPropertyChanged("FullName");
            }
        }
    }
}

There is also a brand new [ICommand] attribute type, which can be used to easily create command properties over methods, by leveraging the relay command types in the MVVM Toolkit. The way this works is simple: just write a method with a valid signature for either RelayCommand, RelayCommand<T>, AsyncRelayCommand or AsyncRelayCommand<T> and add the [ICommand] attribute over it - the generator will create a lazily initialized property with the right command type that will automatically wrap that method. Cancellation tokens for asynchronous commands are supported too! 🚀

Here's an example of how this attribute can be used with four different command types:

Viewmodel definition:
public sealed partial class MyViewModel
{
    [ICommand]
    private void Greet()
    {
    }

    [ICommand]
    private void GreetUser(User user)
    {
    }

    [ICommand]
    private Task SaveFileAsync()
    {
        return Task.CompletedTask;
    }

    [ICommand]
    private Task LogUserAsync(User user)
    {
        return Task.CompletedTask;
    }
}
Generated code:
public sealed partial class MyViewModel
{
    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    private global::Microsoft.Toolkit.Mvvm.Input.RelayCommand? greetCommand;

    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public global::Microsoft.Toolkit.Mvvm.Input.IRelayCommand GreetCommand => greetCommand ??= new global::Microsoft.Toolkit.Mvvm.Input.RelayCommand(new global::System.Action(Greet));

    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    private global::Microsoft.Toolkit.Mvvm.Input.RelayCommand<global::MyApp.User>? greetUserCommand;

    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public global::Microsoft.Toolkit.Mvvm.Input.IRelayCommand<global::MyApp.User> GreetUserCommand => greetUserCommand ??= new global::Microsoft.Toolkit.Mvvm.Input.RelayCommand<global::UnitTests.Mvvm.Test_ICommandAttribute.User>(new global::System.Action<global::UnitTests.Mvvm.Test_ICommandAttribute.User>(GreetUser));
    
    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    private global::Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand? saveFileAsyncCommand;

    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public global::Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand SaveFileAsyncCommand => saveFileAsyncCommand ??= new global::Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand(new global::System.Func<global::System.Threading.Tasks.Task>(SaveFileAsync));
    
    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    private global::Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand<global::MyApp.User>? logUserAsyncCommand;
    
    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public global::Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand<global::MyApp.User> LogUserAsyncCommand => logUserAsyncCommand ??= new global::Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand<global::UnitTests.Mvvm.Test_ICommandAttribute.User>(new global::System.Func<global::UnitTests.Mvvm.Test_ICommandAttribute.User, global::System.Threading.Tasks.Task>(LogUserAsync));
}

Better performance

Another area that I want to investigate with source generators is possibly getting some performance improvemeents by removing reflection where possible. Now, the MVVM Toolkit is already quite light on reflection (as it was designed with that in mind, especially the messenger types), but I think there might be a few places where things could still be improved with source generators. For instance, this method uses quite a bit of reflection.

We could keep this for compatibility and also as a "fallback" implementation, but then we could have the source generator emit a type-specific version of this method with all the necessary handlers already specified, with no reflection. We'd just need to generate the appropriate method in the consuming assembly, and then the C# compiler would automatically pick that one up due to how overload resolution works (since the object recipient in the original method is less specific than a MyViewModel recipient parameter that the generated method would have). Still haven't done a working proof of concept for this point specifically, but it's next on my list and will update as soon as that's done too, just wanted to open this issue in the meantime to start gathering feedbacks and discuss ideas 🙂

EDIT: I've now added a generator that will create a method for this for all types implementing IRecipient<T>:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable
namespace Microsoft.Toolkit.Mvvm.Messaging.__Internals
{
    internal static partial class __IMessengerExtensions
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
        [global::System.Obsolete("This method is not intended to be called directly by user code")]
        public static global::System.Action<IMessenger, object> CreateAllMessagesRegistrator(global::MyApp.MyViewModel _)
        {
            static void RegisterAll(IMessenger messenger, object obj)
            {
                var recipient = (global::MyApp.MyViewModel)obj;
                messenger.Register<global::MyApp.MessageA>(recipient);
                messenger.Register<global::MyApp.MessageB>(recipient);
            }

            return RegisterAll;
        }

        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
        [global::System.Obsolete("This method is not intended to be called directly by user code")]
        public static global::System.Action<IMessenger, object, TToken> CreateAllMessagesRegistratorWithToken<TToken>(global::MyApp.MyViewModel _)
            where TToken : global::System.IEquatable<TToken>
        {
            static void RegisterAll(IMessenger messenger, object obj, TToken token)
            {
                var recipient = (global::MyApp.MyViewModel)obj;
                messenger.Register<global::MyApp.MessageA, TToken>(recipient, token);
                messenger.Register<global::MyApp.MessageB, TToken>(recipient, token);
            }

            return RegisterAll;
        }
    }
}

This is then now picked up automatically when RegisterAll is called, so that the LINQ expression can be skipped entirely.
There are two generated methods so that the non-generic one can be used in the more common scenario where a registration token is not used, and that completely avoids runtime-code generation of all sorts and also more reflection (no more MakeDynamicMethod), making it particularly AOT-friendly 😄

EDIT 2: I've applied the same concept to the other place where I was using compiled LINQ expressions too, that is the ObservableValidator.ValidateAllProperties method. We now have a new generator that will process all types inheriting from ObservableValidator, and create helper methods like this that will then be loaded at runtime by the MVVM Toolkit as above:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable
namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals
{
    [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableValidatorValidateAllPropertiesGenerator", "7.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    [global::System.Obsolete("This type is not intended to be used directly by user code")]
    internal static partial class __ObservableValidatorExtensions
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
        [global::System.Obsolete("This method is not intended to be called directly by user code")]
        public static global::System.Action<object> CreateAllPropertiesValidator(global::MyApp.PersonViewModel _)
        {
            static void ValidateAllProperties(object obj)
            {
                var instannce = (global::MyApp.PersonViewModel)obj;
                __ObservableValidatorHelper.ValidateProperty(instance, instance.Name, nameof(instance.Name));
                __ObservableValidatorHelper.ValidateProperty(instance, instance.Age, nameof(instance.Age));
            }

            return ValidateAllProperties;
        }
    }
}

When the source generators are used, the MVVM Toolkit is now 100% without dynamic code generation! 🎉

Tracking changes so far

  • [INotifyPropertyChangedAttribute]
  • [ObservableObjectAttribute]
  • [ObservableValidatorAttribute]
  • Additional support for generated RegisterAll method when possible
  • Additional support for generated ValidateAllProperties method when possible
  • [ObservableProperty]
  • [AlsoNotifyChangeFor]
  • [ICommand]
  • CanExecute property for [ICommand]
  • [AlsoNotifyCanExecuteFor]
  • Switch to incremental generators

Feedbacks and feature ideas are welcome! 🙌

@Sergio0694 Sergio0694 self-assigned this Mar 22, 2021
@michael-hawker michael-hawker pinned this issue Mar 22, 2021
@michael-hawker
Copy link
Member

We could do simple properties with this too, eh?

[ObservableProperty("MyProperty", typeof(int), "Some Comment about what the property is for doc string.")]

(comment could be optional I guess?)

@Sergio0694
Copy link
Member Author

Sergio0694 commented Mar 23, 2021

Leaving some more info here for @azchohfi regarding the build infrastructure/packaging needed to support this 😄

Including a source generator with the MVVM Toolkit requires us to ship the Microsoft.Toolkit.Mvvm.SourceGenerators project as an analyzer in the Microsoft.Toolkit.Mvvm package. It would not have to be a separate NuGet package (unless we wanted to for some other reason?), so we'd first have to exclude the .Mvvm.SourceGenerators package from being packaged in the CI script. Then the project would need to be packed into the analyzers\dotnet\cs path of the .Mvvm package. In my own library ComputeSharp (which also ships a source generator that is added as an analyzer to projects referencing the library itself from NuGet) I'm doing this:

<PackFolder>analyzers\dotnet\cs</PackFolder>

And then I have a packing project (.msbuildproj) that references both the main lib and the analyzer, and pack that one to actually create the NuGet package. Not sure if there's an easier way, but I can say that's been working pretty well for me so far 🙂

Note for F#/VB.NET devs, there shouldn't be any issues for them other than the analyzer simply not working. The main library should just work as usual without any issues though. I haave an F# test project in my lib and also got some feedbacks from an F# user that could confirm there were no build errors or anything, the analyzer will simply not be triggered outside of C# projects.

Let me know how you want to go about setting this up so we can start shipping usable preview packages for people 😄

@azchohfi
Copy link
Contributor

I believe there is an easier way. Let me quickly investigate this.

@azchohfi
Copy link
Contributor

I think this will do:

MyAnalyzerProject:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

</Project>

MyPackageProject.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CopyAnalyzerProjectReferencesToPackage</TargetsForTfmSpecificContentInPackage>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyAnalyzerProject\MyAnalyzerProject.csproj" PrivateAssets="all" />
  </ItemGroup>

  <Target Name="CopyAnalyzerProjectReferencesToPackage" DependsOnTargets="BuildOnlySettings;ResolveReferences">
    <ItemGroup>
      <TfmSpecificPackageFile Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))">
        <PackagePath>analyzers\dotnet\cs</PackagePath>
      </TfmSpecificPackageFile>
    </ItemGroup>
  </Target>

</Project>

@Sergio0694
Copy link
Member Author

Oh that's awesome! I was sure you'd have some fancier MSBuild magic snippet to make that simpler ahah 😄
Added that in Sergio0694/WindowsCommunityToolkit@618d250, will try that out with a package from the CI once the pipeline finishes running!

@Sergio0694
Copy link
Member Author

Sergio0694 commented Mar 23, 2021

Looks like the CI failed to create the package with this error:

"D:\a\1\s\Windows Community Toolkit.sln" (Pack target) (1) ->
An error occurred when executing task 'Package'.
       "D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj" (Pack target) (18) ->
       (GenerateNuspec target) -> 
         C:\Program Files\dotnet\sdk\5.0.201\Sdks\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets(221,5): error NU5118: File 'D:\a\1\s\Microsoft.Toolkit.Mvvm.SourceGenerators\bin\Release\netstandard2.0\Microsoft.Toolkit.Mvvm.SourceGenerators.dll' is not added because the package already contains file 'analyzers\dotnet\cs\Microsoft.Toolkit.Mvvm.SourceGenerators.dll' [D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj]
         C:\Program Files\dotnet\sdk\5.0.201\Sdks\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets(221,5): error NU5118: File 'D:\a\1\s\Microsoft.Toolkit.Mvvm.SourceGenerators\bin\Release\netstandard2.0\Microsoft.Toolkit.Mvvm.SourceGenerators.dll' is not added because the package already contains file 'analyzers\dotnet\cs\Microsoft.Toolkit.Mvvm.SourceGenerators.dll' [D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj]
         C:\Program Files\dotnet\sdk\5.0.201\Sdks\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets(221,5): error NU5118: File 'D:\a\1\s\Microsoft.Toolkit.Mvvm.SourceGenerators\bin\Release\netstandard2.0\Microsoft.Toolkit.Mvvm.SourceGenerators.pdb' is not added because the package already contains file 'analyzers\dotnet\cs\Microsoft.Toolkit.Mvvm.SourceGenerators.pdb' [D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj]
         C:\Program Files\dotnet\sdk\5.0.201\Sdks\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets(221,5): error NU5118: File 'D:\a\1\s\Microsoft.Toolkit.Mvvm.SourceGenerators\bin\Release\netstandard2.0\Microsoft.Toolkit.Mvvm.SourceGenerators.pdb' is not added because the package already contains file 'analyzers\dotnet\cs\Microsoft.Toolkit.Mvvm.SourceGenerators.pdb' [D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj]
         C:\Program Files\dotnet\sdk\5.0.201\Sdks\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets(221,5): error NU5118: File 'D:\a\1\s\Microsoft.Toolkit.Mvvm.SourceGenerators\bin\Release\netstandard2.0\Microsoft.Toolkit.Mvvm.SourceGenerators.xml' is not added because the package already contains file 'analyzers\dotnet\cs\Microsoft.Toolkit.Mvvm.SourceGenerators.xml' [D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj]
         C:\Program Files\dotnet\sdk\5.0.201\Sdks\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets(221,5): error NU5118: File 'D:\a\1\s\Microsoft.Toolkit.Mvvm.SourceGenerators\bin\Release\netstandard2.0\Microsoft.Toolkit.Mvvm.SourceGenerators.xml' is not added because the package already contains file 'analyzers\dotnet\cs\Microsoft.Toolkit.Mvvm.SourceGenerators.xml' [D:\a\1\s\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj]

The fact we're getting two duplicate files here for each separate file, when the MVVM Toolkit itself has 3 targets, makes me think the issue is that the analyzer is being packed again when each individual target is being built and added to the package, instead of just once for all 3 targets. I've pushed a new commit with an extra condition for that <Target/> element so that it's only triggered when the .NET Standard 2.0 target is selected (the analyzer should be shared by all 3 anyway), let's see if it does the trick 😄

EDIT: that worked! 🎉🎉🎉

image

Everything looks absolutely great from dotPeek! 🚀

EDIT 2: preview package is now out! First one is Microsoft.Toolkit.Mvvm.7.0.0-build.1082.g849a01c6e3 🥳

@ismaelestalayo
Copy link

We could do simple properties with this too, eh?

[ObservableProperty("MyProperty", typeof(int), "Some Comment about what the property is for doc string.")]

Could an initial default value for the property be added there as well?
90% of the times I set up a default value for my Observable properties:

public class User : ObservableObject {
    private string name = "foo";
    public string Name {
        get => name;
        set => SetProperty(ref name, value);
    }
}

@Sergio0694
Copy link
Member Author

Assuming the initial value is a compile-time constant (as that's a limitation with attribute arguments), then yeah that would be doable. Still not 100% sure how I feel about declaring properties through attributes, but I can definitely give it a try 😄

@Sergio0694
Copy link
Member Author

@michael-hawker @ismaelestalayo I've reworked the proposal a bit due to the fact that using attributes on types means you have to be more verbose for the type itself, can't use it if you also want to access a type argument in the type, and also have no control on the field name. Instead I've added an [ObservableProperty] attribute that can be applied directly on a field, and can also automatically determine the property name based on the field name. This also gives developers the ability to choose the naming convention they prefer for field names. Also, this makes it very easy to add XML docs on a property, as we can just copy and tweak the XML docs applied to fields. This is what I have working so far:

User code:

public partial class SampleModel : ObservableObject
{
    [ObservableProperty]
    private int data;
}

Generated code:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace UnitTests.Mvvm
{
    public partial class SampleModel
    {
        [DebuggerNonUserCode]
        [ExcludeFromCodeCoverage]
        public int Data
        {
            get => data;
            set
            {
                if (!EqualityComparer<int>.Default.Equals(data, value))
                {
                    OnPropertyChanging();
                    data = value;
                    OnPropertyChanged();
                }
            }
        }
    }
}

Note how the name is automatic here, in this case it's the field name with the first character in uppercase. I'm also trimming field names like _foo (with the same capitalization after trimming, and m_foo for those who love the C++ naming convention 😄

I also want to make it so that if you're only implementing INotifyPropertyChanged in the parent class, OnPropertyChanging is omitted automatically, which gives consumers additional flexibility. Thoughts? 🙂

@jtippet
Copy link

jtippet commented Mar 28, 2021

One gotcha I've discovered is that the compiler is unhappy putting virtual members into a sealed class:

    [ObservableObject]
    internal sealed partial class Demo
    {
        private int _x;
        public int X
        {
            get => _x;
            set => SetProperty(ref _x, value);
        }
    }

gives error

Error CS0549 'Demo.OnPropertyChanging(PropertyChangingEventArgs)' is a new virtual member in sealed type 'Demo'	ObservableObjectDemo

because the codegen hardcodes a virtual on a few members. I don't suppose the codegen could notice this case and remove the virtual keyword?

As an aside, I wonder if each generated method should also have [GeneratedCodeAttribute] on them?

Regarding [ObservableProperty], it would save me even more lines of code, which I really appreciate. (How is this not built in to the language yet?!) Although it "feels" a bit weird to emphasize the field and not the property. I don't know anything about how code generators work, so maybe this is impossible, but I'd prefer to invert things so you write the property and generate the field. Something like:

    internal partial class Demo
    {
        [ObservableProperty]
        public int X { get; set; }

        [ObservableProperty]
        public int Y { get; private set; } = 42
    }

to generate

    internal partial class Demo
    {
        private field _x;
        public int X { get => _x; set => SetProperty(ref _x, value); }

        private int _y = 42;
        public int Y { get => _y; private set => SetProperty(ref _y, value); }
    }

That is much closer to how the compiler-generated fields work already, and it also gives a natural and familiar place to put private or protected. But it might just be flat-out impossible to do with code generation; you might need IL rewrite instead. (Maybe C# 10 will have XAML apps as a theme, and throw a bone to INotifyPropertyChanged...)

@Sergio0694
Copy link
Member Author

"[...] the compiler is unhappy putting virtual members into a sealed class [...]"

Oh, good catch! Yeah I'll have to add special handling for that and strip virtual when the target class is sealed. Thanks!

"I wonder if each generated method should also have [GeneratedCodeAttribute] on them?"

I could definitely do that, just wondering how useful that would be? Is that attribute commonly used? 🤔
For now I'm adding [DebuggerNonUserCode] and [ExcludeFromCodeCoverage].

"[...] but I'd prefer to invert things so you write the property and generate the field."

That's unfortunately not possible with source generators, as they're explicitly additive and can't modify/rewrite existing code.
They could in the original proposal but has since been changed to make them a compiler-only feature without the need to also update the C# language to support it (as there would've been the need of new keywords to indicate rewritable members).

@jtippet
Copy link

jtippet commented Mar 28, 2021

I could definitely do that, just wondering how useful that would be?

It seems like it would suppress certain types of suggestions. E.g., I don't really need the compiler telling me that "object initialization could be simplified" in generated code.

@Sergio0694
Copy link
Member Author

That should already not be a problem, since all the generated files here will have #pragma warning disable 🤔

@jtippet
Copy link

jtippet commented Mar 28, 2021

Ah, fair enough, the #pragma might have the same effect.

@Sergio0694
Copy link
Member Author

Yeah, that just suppresses all warnings in the file, I'm adding that to avoid cases where eg. a consumer might have some custom StyleCop settings in the project which might not match exactly the style in the generated code. This way we're sure that no warnings will ever be generated from code coming from the source generator shipped in the package 🙂

@Sergio0694
Copy link
Member Author

Small update (cc. @jtippet) as of CommunityToolkit/WindowsCommunityToolkit@764d49b

✅ Added handling for sealed class (members will become private, and no virtual anymore)
✅ Added [GeneratedCode] attribute to all members (indicating generator type and version)

Plus a bunch of other improvements and tweaks 😄

@jtippet
Copy link

jtippet commented Mar 29, 2021

Soo... when I write some good code, it annoys me when people say "yes but..." and proceed to demand new features. Well I'm about to inflict the same sort of demand on you. Sorry.

In a few cases, I can't use [ObservableProperty], because I need to do something when the value changes. I could subscribe to my own PropertyChanged event, but that's not super efficient. An efficient alternative is to add new attributes that let me drop in a bits of code:

        [ObservableProperty]
        [OnChanging(nameof(ValidateThing))]
        [OnChanged(nameof(LogThingChange))]
        private Thing _thing;

which would expand out to something like:

        private Thing _thing;
        public Thing Thing
        {
            get => _thing;
            set
            {
                if (!EqualityComparer(_thing, value))
                {
                    ValidateThing(nameof(Thing), newValue); // injected by OnChangingAttribute
                    OnPropertyChanging();
                    var oldValue = _thing;
                    _thing = value;
                    LogThingChange(nameof(Thing), oldValue, value); // injected by OnChangedAttribute
                    OnPropertyChanged();
                }
            }
        }

That would make it easy to drop in some validation logic, logging, or a rebuild-cache type callback.

If you're not already tired of adding new attributes, one other specific thing I would do in these callbacks would be to [un]subscribe events. For example:

        private Thing _thing;
        public Thing Thing
        {
            get => _thing;
            set
            {
                if (!EqualityComparer(_thing, value))
                {
                    if (_thing is not null)
                    {
                        _thing.PropertyChanged -= OnThingPropertyChanged;
                    }

                    _thing = value;
                    OnPropertyChanged();

                    if (_thing is not null)
                    {
                        _thing.PropertyChanged += OnThingPropertyChanged;
                    }
                }
            }
        }

That's formulaic enough that it could all be stuffed into a single attribute:

        [ObservableProperty]
        [PropertyEventSubscription(nameof(Thing.PropertyChanged), nameof(OnThingPropertyChanged))]
        private Thing _thing;

Ideally, these 3 attributes have AllowMultiple = true and generate efficient code if you use multiple of them on the same field.

But your proposed feature is already very useful on its own -- you don't need to add these features, and it's perfectly okay to say "interesting idea, maybe in v9".

@ismaelestalayo
Copy link

Following @jtippet 's point, I do think a way to override the setter of the property would be nice. In my case, I dont do much validation, but sometimes I doupdate one property from another property's setter:

internal class Contact {
    private field _age = 0;
    public int Age { get => _x; set => SetProperty(ref _x, value); }

    private DateTime _birthDay;
    public DateTime Birthday {
        get => _y;
        set {
            SetProperty(ref _y, value);
            // Calculate the age of the contact
            // and update its age 
        }
    }
}

@Sergio0694
Copy link
Member Author

Sergio0694 commented Mar 29, 2021

Thanks @jtippet and @ismaelestalayo for the additional feedbacks 🙂

There's one thing I should point out here, there's a balance we need to strike between how many new features we want to add, and how overcomplicated the implementation becomes, and also how "weird" the code becomes when generators are involved specifically. At least for a first release with them, I'd like to just stick to common functionality that already exists, but making it more modular, as well as some new small functionality that's minimal enough. This is where the [ObservableProperty] comes into play: it's already "unconventional" in the way that it lets you define observable properties by annotating a field, but it's still simple enough to be intuitive to use. If we start to add too many custom features that would only be usable in very specific cases, we quickly make the whole thing much more complex for no real gain. I'd suggest that in these cases you'd be better off just implementing your observable property manually as usual, which would give you full control, rather than trying to get too many custom and very niche attributes into the actual MVVM Toolkit. Hope this makes sense 😄

That said, I'm thinking of at least adding some way to specify dependent properties across different properties, so that you'd be able to indicate that a property should also raise the notification events for other properties when its value is changed. This would be something that would also help in the scenario that @ismaelestalayo is describing, something like:

internal partial class Contact : ObservableObject
{
    [ObservableProperty]
    [AlsoNotifyFor(nameof(Age))]
    private DateTime birthday;

    public int Age => CalculateAgeFromBirthday(Birthday);
}

Which would generate something like this:

internal partial class Contact : ObservableObject
{
    [ObservableProperty]
    [AlsoNotifyFor(nameof(Age))]
    public DateTime Birthday
    {
        get => birthday;
        set
        {
            if (!EqualityComparer<DateTime.Default.Equals(birthday, value))
            {
                OnPropertyChanging();
                birthday = value;
                OnPropertyChanged();
                OnPropertyChanged("Age");
            }
        }
    }
}

I'll experiment with this in the next few days and see how it goes. More feedbacks are always welcome of course 🙂

EDIT: the new [AlsoNotifyFor] attribute is now implemented and can be tested with the latest preview CI packages 😄

@timunie
Copy link

timunie commented Mar 31, 2021

Hi @Sergio0694

do you see any chance (or is it even a good idea) to have an async validation running? I would like to test if a file exists and inform the user if not. My first thought was running a CustomValidation but unfortunately this will make the UI unresponsible if the file is on a network share which is not connected.

I updated my demo to provide a test case: https://github.com/timunie/MvvmToolkitValidationSample

// This is the property to validate
private int _ValidateAsyncExample;
[CustomValidation(typeof(ViewModel), nameof(LongRunningValidation))]
public int ValidateAsyncExample
{
    get { return _ValidateAsyncExample; }
    set { SetProperty(ref _ValidateAsyncExample, value, true); }
}

 // This is my validator
public static ValidationResult LongRunningValidation(int value, ValidationContext _)
{
    if (value % 2 == 0)
    {
        return ValidationResult.Success;
    }
    else
    {
        Task.Delay(1000).Wait();
        return new ValidationResult("The Value must be even.");
    }
}

My idea would be to have something like ValidatePropertyAsync(object value, string PropertyName)

Happy coding
Tim

@Sergio0694
Copy link
Member Author

Hi @timunie, indeed running any kind of IO operation synchronously is always a recipe for disaster, as your example shows 😄

The main issue here is that the validation support in the MVVM Toolkit is closely following the official specification from the [ValidationAttribute] type in the BCL and all the accompanying classes (this is due to the MVVM Toolkit having the idea of being a "reference implementation" as one of its core principles, so we're following the BCL and the standard types as much as possible). As you can see from the .NET API browser, this attribute is strictly meant to be used for quick, synchronous validation scenarios, and it doesn't really support asynchronous operations. This goes for companion APIs such as Validator too, which we're using in the MVVM Toolkit as well. I think that for asynchronous custom validation methods you should probably do these checks manually through the pattern of your choice (eg. possibly by writing some custom control/extension to support this).

Hope this makes sense 🙂

@timunie
Copy link

timunie commented Mar 31, 2021

Hi @timunie, indeed running any kind of IO operation synchronously is always a recipe for disaster, as your example shows 😄

The main issue here is that the validation support in the MVVM Toolkit is closely following the official specification from the [ValidationAttribute] type in the BCL and all the accompanying classes (this is due to the MVVM Toolkit having the idea of being a "reference implementation" as one of its core principles, so we're following the BCL and the standard types as much as possible). As you can see from the .NET API browser, this attribute is strictly meant to be used for quick, synchronous validation scenarios, and it doesn't really support asynchronous operations. This goes for companion APIs such as Validator too, which we're using in the MVVM Toolkit as well. I think that for asynchronous custom validation methods you should probably do these checks manually through the pattern of your choice (eg. possibly by writing some custom control/extension to support this).

Hope this makes sense 🙂

Yeah absolutely. I thought already that this will be the case. If anyone else comes across such a situation: I solved it via another helper property of type bool? which indicates the state of the validation. true if the file available, false if the file is not found, null while checking the filename async.

Happy coding and happy easter
Tim

@chrisk414
Copy link

The MVVM Toolkit is UX Framework agnostic, so it wouldn't fit in the existing CommunityToolkit.MVVM .NET Standard package.

It looks like the new-kinds-in-the-block (MAUI) decided to call it BindableProperty, breaking the nomenclatures. Too bad and perhaps the code generator is smart enough to be able to generate different codes for a different framework to make it framework agnostic.
But it's cool to know HavenDV already has the solution.

One thing I was curious about is, how will the debugging work with so many codes are generated behind? Will breakpoint work for instance? If I want to break on the Setter, I'm not even sure where to put the breakpoint on. ^^
Perhaps that's the price we have to pay.

@michael-hawker
Copy link
Member

@chrisk414 underneath Dependencies in your project in VS, you can open up Analyzers find the source generator that's generating code, and open the generated file, I believe as long as 'Just My Code' is disabled then it should allow you to set breakpoints and stop in the generated code there, like anything else.

@HavenDV
Copy link
Contributor

HavenDV commented Jul 9, 2022

It looks like the new-kinds-in-the-block (MAUI) decided to call it BindableProperty, breaking the nomenclatures. Too bad and perhaps the code generator is smart enough to be able to generate different codes for a different framework to make it framework agnostic. But it's cool to know HavenDV already has the solution.

One thing I was curious about is, how will the debugging work with so many codes are generated behind? Will breakpoint work for instance? If I want to break on the Setter, I'm not even sure where to put the breakpoint on. ^^ Perhaps that's the price we have to pay.

Yes, MAUI uses its own classes to create Dependency properties and the generator doesn't support that at the moment. I'll add partial MAUI support today, but I'll need help testing it out because I don't have MAUI projects right now. Also at the moment I don't know how to ensure the correct configuration for MAUI and check diagnostics using Roslyn, so the work will be done blindly. On the plus side, MAUI has one method signature for all kinds of properties, which makes the task easier.

P.S. I have released a version with MAUI support. Some MAUI-specific features are not yet supported: DefaultValueCreator, PropertyChanging, DefaultBindingMode, sender in ValidateValue callback

@pekspro
Copy link

pekspro commented Jul 14, 2022

Sometimes I would cache the value of a property and source generators could be useful for this. Let’s say you write this code:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Message))]
string _Name;

[CachedObservableProperty]
string GetMessage()
{
  return $"Hello {Name}";
}

And then this code is generated:

public string Name
{
  get => _Name;
  set
  {
    if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(_Name, value))
    {
      OnNameChanging(value);
      OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name);
      _Name = value;
      // Clear cache
      MessageIsCached = false;
      OnNameChanged(value);
      OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name);
      OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Message);
    }
  }
}

protected bool MessageIsCached;
protected string MessageCachedValue;

public string Message
{
  get
  {
    if (!MessageIsCached)
    {
       MessageCachedValue = GetMessage();
       MessageIsCached = true;
    }

    return MessageCachedValue;
  }
}

Cannot say I would use it very often :-) But sharing it here for feedback.

@CBenghi
Copy link

CBenghi commented Jul 15, 2022

Hello,
I've got a little suggestion for even smarter code generation; consider the following code, where a command can execute depends on the specified member CanDoSomething:

public partial class Feedback : ObservableObject
{
	[ObservableProperty]
	[NotifyPropertyChangedFor(nameof(CanDoSomething))]
	[NotifyCanExecuteChangedFor(nameof(DoSomethingCommand))] // redundant, in the definition of RelayCommand we know that CanDoSomething should trigger CanExecuteChanged on the command
	private string fileName1 = "";

	[ObservableProperty]
	[NotifyPropertyChangedFor(nameof(CanDoSomething))]
	[NotifyCanExecuteChangedFor(nameof(DoSomethingCommand))] // redundant, in the definition of RelayCommand we know that CanDoSomething should trigger CanExecuteChanged on the command
	private string fileName2 = "";

	public bool CanDoSomething => File.Exists(fileName1) && File.Exists(fileName2);
		
	[RelayCommand(CanExecute = nameof(CanDoSomething))]
	public void DoSomething() {}
}

My suggestion is to automate the NotifyCanExecuteChangedFor, which can be inferred because we know the command depends on changes on CanDoSomething, so every automation that changes CanDoSomething should trigger it.

I know it's only two lines saved in this scenario, but on more complex viewmodels it would allow the developers to relax and know that whenever CanDoSomething is changed the command's CanExecute is re-evaluated.

If you are interested in the idea I'd be happy to propose a PR.

@kamil-cupial
Copy link

Hi,

It looks like source generators produce errors on newer SKDs. When building with .NET6 SDKs newer than 6.0.101. I get a bunch (500+) of compilation errors. This does not occur if I build with 6.0.101.
Tested with SDK 6.0.3, 6.0.107 and preview-4 and 3.

All errors look like these:

[project path]\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator\[namespace and file name].cs(170,22): error CS0111:
Type '[class name]' already defines a member called 'On[property]Changed' with the same parameter types [[project path]\[project name]_wwuqb4gk_wpftmp.csproj]

error CS0756: A partial method may not have multiple defining declarations

error CS0535: '[class].TaskNotifier<T>' does not implement interface member '[class].ITaskNotifier<Task<T>>.Task'

error CS0102: The type '[class]' already contains a definition for 'ITaskNotifier'

This is not just about ITaskNotifier but I get duplicates about OnChanged/Changing methods etc. too.

@JorgeCandeias
Copy link

It looks like source generators produce errors on newer SKDs.

This is an sdk regression tracked here (dotnet/wpf#6792), identified here (dotnet/wpf#6792 (comment)) and there are workarounds here (dotnet/wpf#6792 (comment)) and here (dotnet/wpf#6792 (comment)) until the fix (dotnet/wpf#6793) is available.

@MisinformedDNA
Copy link

From what I'm reading, these attributes are supposed to be for 8.x, but I am seeing some of these in 7.1. Are these supposed to be there? Are they stable to use?

image
image

@michael-hawker
Copy link
Member

@MisinformedDNA we shipped a preview of the source generators in 7.1.x. However, they're built on top of the previous version of the Source Generator API from .NET, so they fall over with larger loads. In retrospect we probably should have separated them out as a separate package, but we didn't quite realize the impact at the time.

These new generators with 8.0 have been refined and completely rebuilt with the new Incremental generator API, so they'll work under any sized project. I know Sergio is planning a release in the near future, and I don't think there's any planned breaking changes since the last preview release.

Did I miss anything @Sergio0694?

@Sergio0694
Copy link
Member Author

Sergio0694 commented Jul 20, 2022

Nope, that's exactly right. I would strongly recommend not to use the attributes in 7.1. They're not supported for production. If you want to use the generators, do use CommunityToolkit.Mvvm 8.0.0, which in Preview 4 is basically RTM anyway. We'll hopefully release a stable version of it by the end of the month 🙂

@MisinformedDNA
Copy link

The preview version doesn't exist under the Microsoft.Toolkit.Mvvm NuGet package. After reading the roadmap, it seems like everything is moving to the CommunityToolkit prefix, is that correct?

@Sergio0694
Copy link
Member Author

Yup that's correct, the new package/namespace will be CommunityToolkit 🙂
I'll document this in the blog post too, and will deprecate the Microsoft.Toolkit.* packages too to avoid confusion.

NOTE: the package is still published and maintained by Microsoft, it's just a name change.

@MisinformedDNA
Copy link

Are all the CommunityToolkit repos maintained by Microsoft? Or just the MVVM one? Seems weird to get rid of the Microsoft.Toolkit prefix. I read that it is due to reserving NuGet package prefixes, but if Microsoft owns it, I don't see the problem.

@Sergio0694
Copy link
Member Author

"Are all the CommunityToolkit repos maintained by Microsoft?"

As far as I know, yes. For sure the .NET, Windows and MAUI Community Toolkit-s are.

"I read that it is due to reserving NuGet package prefixes, but if Microsoft owns it, I don't see the problem."

It just makes things simpler for us for several reasons we don't have to go into 🙂
The publisher (on NuGet as well) is still Microsoft anyway. It's really just a name change, that's it.
Like, we're also migrating to the new CommunityToolkit.* repos in our first party apps (eg. the Store). Nothing changed 😄

@timunie
Copy link

timunie commented Jul 26, 2022

It just makes things simpler for us for several reasons we don't have to go into 🙂

Just to add from my side: I think for people like me who are using Avalonia or any other not MS UI framework may wonder if they can use something that has Microsoft.* in the name. So I like the new name tbh.

Happy coding
Tim

@chylex
Copy link

chylex commented Aug 5, 2022

Could it be possible for [ObservableProperty] to generate a non-public setter? I have several models with properties that have a private or internal setter, and I don't see a way to replace the public setter that [ObservableProperty] generates now.

@timunie
Copy link

timunie commented Aug 5, 2022

Could it be possible for [ObservableProperty] to generate a non-public setter? I have several models with properties that have a private or internal setter, and I don't see a way to replace the public setter that [ObservableProperty] generates now.

That would be useful for read-only properties, yes. I vote for it :-). Let me add: At least in Avalonia one can bind to internal properties. So maybe properties can also be made internal on request. I don't use this feature of Avalonia until today, but probably it's worth to mention here.

@chylex
Copy link

chylex commented Aug 5, 2022

Could it be possible for [ObservableProperty] to generate a non-public setter? I have several models with properties that have a private or internal setter, and I don't see a way to replace the public setter that [ObservableProperty] generates now.

That would be useful for read-only properties, yes. I vote for it :-). Let me add: At least in Avalonia one can bind to internal properties. So maybe properties can also be made internal on request. I don't use this feature of Avalonia until today, but probably it's worth to mention here.

Both the getter and setter visibility could be properties of the attribute, maybe something like:

[ObservableProperty(Getter = Visibility.Internal, Setter = Visibility.Private)]

If there's interest and we can work out the details of usage and naming, I can make a PR - or just leave it to the maintainers, whichever way would be easier for them.

EDIT: See my comment below.

@pekspro
Copy link

pekspro commented Aug 5, 2022

@chylex, this has been suggested earlier:

#291

(spoiler alert: maybe in C# 12)

@chylex
Copy link

chylex commented Aug 5, 2022

@chylex, this has been suggested earlier:

#291

(spoiler alert: maybe in C# 12)

Quoting the reply from #291:

This is not supported and not planned. We want to support this properly in C# 12 via partial properties. For now the plan is to work on a proposal for that to add to the working set, and I don't think it'd be a good idea to hack a clunkier alternative for this just for the short term until C# 12 was out (which would be november 2023). That would also solve a whole lot of other problems related to the fact we're currently using fields in the first place 🙂

Personally I disagree that november 2023 is 'short term', and if the solution are partial properties instead of fields then that sounds like it's going to be a breaking change for users of Community Toolkit anyway.

I might try to implement this anyway. If the maintainers don't want to accept a PR and officially support this feature, which would be understandable, I can just put it on a self-hosted NuGet repository and use it in my own projects.

EDIT: I added the feature in my fork, if anyone wants it feel free to build a custom version from this commit: chylex@0d941a6

If anyone needs help with building and/or creating a custom NuGet repository, feel free to open a discussion in my fork, if there is actually interest I will write a guide. Please don't post any comments in this topic or anywhere in the official repository, I don't want to bother the maintainers.

@Sergio0694
Copy link
Member Author

Sergio0694 commented Aug 5, 2022

"if the solution are partial properties instead of fields then that sounds like it's going to be a breaking change for users of Community Toolkit anyway"

I didn't say support for fields would be removed. We can very well consider just leaving it there side by side by properties, or at least keeping it there for one release cycle to give everyone time to migrate to properties. Plus, this is not a show stopper. If you absolutely need a private setter for the time being, you can use manual properties in those cases, and still use generated properties everywhere else 🙂

"I might try to implement this anyway"

Don't do this. As the PR template says, PRs without an approved linked proposal will just be closed.


Closing this given the source generators have now officially shipped 😄
We can use individual issues for bugs/proposals, and I might open a new tracking issue for the next major release.

@michael-hawker
Copy link
Member

The preview version doesn't exist under the Microsoft.Toolkit.Mvvm NuGet package. After reading the roadmap, it seems like everything is moving to the CommunityToolkit prefix, is that correct?

You can find more information about this from one of our blog posts, to quote:

... having our [own] package identity allows us to better maintain our packages and their ownership with the community. In the future, it also allows us more flexibility to work with other groups and technologies to share branding and ownership of packages.

@todor-dk
Copy link

Don't do this. As the PR template says, PRs without an approved linked proposal will just be closed.

Fair enough. And it is now too late to to get this wish in the release, what would have been the best procedure to get the change in an official release. The change that @chylex did is very desirable and I've considered it myself.

In other words, how would one have created an approved linked proposal, so a PR can get through.

@chylex
Copy link

chylex commented Oct 12, 2022

A proposal already existed and was denied, so I don't think there was any way for a PR to get through. If the maintainers decided to allow the proposal, I would still be interested in making a PR for the benefit of others, but for now I would recommend what I did - build a custom version from my fork, and create a private NuGet repository.

If it helps, I can upload my NuGet repository somewhere, since it is made entirely of static files so you'd just need to upload it to a web server. Or use my repository, but I can't guarantee its uptime or which versions of the toolkit would be available. Details would be best discussed in my fork's discussions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 💡 A new feature being implemented improvements ✨ Improvements to an existing functionality mvvm-toolkit 🧰 Issues/PRs for the MVVM Toolkit open discussion ☎️ An issue open for active community discussion optimization ☄ Performance or memory usage improvements planning 📑 An issue tracking planning around a milestone
Projects
None yet
Development

No branches or pull requests