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

Add support for [ObservableProperty] on partial properties #555

Closed
7 tasks done
Sergio0694 opened this issue Jan 4, 2023 · 56 comments · Fixed by #966
Closed
7 tasks done

Add support for [ObservableProperty] on partial properties #555

Sergio0694 opened this issue Jan 4, 2023 · 56 comments · Fixed by #966
Labels
feature request 📬 A request for new changes to improve functionality mvvm-toolkit 🧰 Issues/PRs for the MVVM Toolkit

Comments

@Sergio0694
Copy link
Member

Sergio0694 commented Jan 4, 2023

Overview

This issue tracks adding support for partial properties to the [ObservableProperty] generator. This is blocked on dotnet/csharplang#6420, so support for this will have to wait for that first (hopefully in the C# 12 timeframe).

API breakdown

The changes are pretty straightforward: properties will also be allowed on [ObservableProperty]:

-[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
 public sealed class ObservablePropertyAttribute : Attribute
 {
 }

Analyzers will also need to be added to ensure that the annotated properties are partial and valid.

Usage example

Code like this:

[ObservableProperty] 
public partial string Name { get; private set; }

Will generate the following:

/// <inheritdoc cref="_itemsSource"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial string Name
{
    get => field;
    set
    {
        if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(field, value))
        {
            OnNameChanging(value);
            OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name);
            
            field = value;

            OnItemsSourceChanged(value);
            OnNameChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name);
        }
    }
}

/// <summary>Executes the logic for when <see cref="Name"/> is changing.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
partial void OnNameChanging(string value);

/// <summary>Executes the logic for when <see cref="Name"/> just changed.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
partial void OnNameChanged(string value);

This will also enable support for declaring custom accessibility modifiers for property accessors.

Progress tracking

  • Initial code generation support
  • Add analyzer for C# preview
  • Add analyzer to use partial properties instead of fields
  • Add code fixer to convert fields to partial properties
  • Add analyzer for partial property declarations
  • Add analyzer for unsupported Roslyn version for properties
  • Update all titles/descriptions of existing shared diagnostics
@cmiles
Copy link

cmiles commented May 18, 2023

@Sergio0694 I haven't tried to track a language proposal before and couldn't immediately tell from the Partial Properties proposal/labels/information whether it is still expected to be available in the November 2023 time frame that you mentioned in #291 - since you have been tracking and adding to the the Partial Properties proposal/discussion any chance you can add any current information about time frame estimates here?

Fwiw From what I've read here and in other issues Partial Properties seem like a great solution - just wondering if it is truly 'coming soon' or if this pushes into 2024 and beyond.

Thanks for your time and any update!

@Sergio0694
Copy link
Member Author

Hey @cmiles! We don't have any updates/ETAs to share at this time. Keep in mind that no C# features at all are confirmed until the moment they actually ship, so the same applies here. We're still actively looking into this though, just no news just yet 🙂

@modmynitro
Copy link

Its not in C# 12
dotnet/csharplang#6420 (comment)

@cmiles
Copy link

cmiles commented Oct 21, 2023

@Sergio0694 since this did not make it into C#12 any chance that of any of the associated issues will be reconsidered for action in a more immediate time frame?

I understand partial properties would facilitate the ideal implementation of things like 'Required' keyword support - but there is some functionality blocked by this that would be immediately useful and appreciated even if there were breaking changes in the future if/when partial properties make it into C#.

@Sergio0694
Copy link
Member Author

"but there is some functionality blocked by this"

What functionality is blocked?

@cmiles
Copy link

cmiles commented Oct 22, 2023

What functionality is blocked?

@Sergio0694 - It was my understanding that all of the issues/functionality listed below are blocked by wanting to wait and create an implementation based around the partial property language feature.

For me the functionality the required keyword offers is particularly attractive as we work on enabling nullable support in our code - but I'm not aware of any way to combine [ObservableProperty] and required?

Based on your question of 'What functionality is blocked?' maybe I am mistaken and there is a feature or approach that I'm not aware of to use [ObservableProperty] and required together - if so apologies for the wasted time/space in this issue but would very much appreciate a link to get me started!

Add support for "required" and "virtual" keywords to property generator · Issue #679 · CommunityToolkit/dotnet
Make ObservableProperty Generated Code Properties Virtual? · Issue #543 · CommunityToolkit/dotnet
Add support for setter protection level for ObservableProperty · Issue #758 · CommunityToolkit/dotnet
Add abilities to control the access level of generated [ObservableProperty] attribute · Issue #585 · CommunityToolkit/dotnet

@Sergio0694
Copy link
Member Author

"all of the issues/functionality listed below are blocked"

"For me the functionality the required keyword offers is particularly attractive"

No functionality is actually blocked. You can easily use all that — just manually define the properties 🙂

I understand the frustration for wanting a simpler way to do that, and I do agree that of course using the generator is better. But I don't think it's a good idea to dedicate resources to implement all that functionality when we might very well have partial properties out in preview soon, especially because then we'd still have to keep supporting that "workaround" to avoid breaking consumers. Rather, it makes more sense to just wait for partial properties to be available in preview, add support for those, and then release a preview of the MVVM Toolkit too so that people wanting to try those out can do so as well.

@cmiles
Copy link

cmiles commented Oct 23, 2023

@Sergio0694 No frustration here - seems like we just have a different evaluation of the issue.

Fwiw I would still '+1' working on a solution now - but respect and understand your reasoning and decision and no doubt you have 100% better insight on the odds of partial properties arriving in a near future preview.

I hope this issue will continue to be re-evaluated if for some reason partial properties, or something equivalently useful for generators, don't arrive for the next version of C#.

Thanks for the prompt replies - appreciated!

@jhm-ciberman
Copy link

This was not added to C# 12. Is there any reason to not implement the other more traditional alternatives using attributes?

@Sergio0694
Copy link
Member Author

This exact question and the answer for that are in the messages right above yours — the reason has not changed and it's the same as before 🙂

@AignerGames
Copy link

Partial properties are just in proposel state, it's not confirmed that they will be release any time soon (or at all). I don't think that it is a good idea to wait for some unknown future time and close all releated issues.

I understand your argument, that the current solution is a workaround, but it's a workaround that already exists, so adding more features to the already existing / working "workaround" would be better for every current user of the toolkit. You can declare it obsolete as soon as partial properties are implemented (if ever) and then you don't have to worry about maintaning it in the future.

Would it really take that many resources to add a additional parameter to control the accessor kind of the generated property to justify waiting for partial properties?

@DennisWelu
Copy link

@AignerGames is right on. I’ve had similar thoughts in the past about this issue. It could be years or never, yet the value is high and the effort seems at least relatively low. Worth the trouble of a depreciation period if needed.

@jhm-ciberman
Copy link

Personally, I don't use generators in MVVM Toolkit at all only for this sole issue.

@jhorv
Copy link

jhorv commented Apr 27, 2024

@Sergio0694 frustration here.

The fact that [ObservableProperty]s are public-public causes me to barely use it. If only I could specify a private set, then almost every one of my view models could have its lines of code cut in half. What's the point of deriving from ObservableObject if I'm always the one calling SetProperty()? I could have that with an extension method, for crying out loud.

It's becoming increasingly difficult to convince people (and believe myself) that I did a smart thing in choosing this library. To me it doesn't matter how, whether via attribute or some future C# language feature. Just add the ability and make these features useful for more than just demos.

@Sergio0694
Copy link
Member Author

Generating a partial property wouldn't really do anything, other than making your code fail to compile 🙂

As for the real support in the MVVM Toolkit, I'm waiting to see whether field will make it as well.

@LE-MarkusW
Copy link

Ok, then my knowledge is not reaching far enough. I hope something like this can be implemented though.
I would need to annotate the generated Properties with Attributes for EF Core. But since the Navigation Properties have to be virtual anyway, I will probably have to o it by hand for the foreseeable Future anyway.

@LE-MarkusW
Copy link

LE-MarkusW commented Jul 18, 2024

Generating a partial property wouldn't really do anything, other than making your code fail to compile 🙂

Ok I played around with the Preview a little bit. I guess the Introduction of [PartialProperty] and [VirtualProperty] would probably be necessary for my use case. 😞

@cmiles
Copy link

cmiles commented Jul 18, 2024

@LE-MarkusW I don't really know your use case but if you haven't already it might be worth your time to look at Metalama for code generation. Metalama is a paid tool with a free tier - Pricing, License Information.

I used it to write my own INotifyPropertyChanged a little over a year ago in order to take advantage of the required keyword and and now use it for a few other generation tasks including Commands. For my simple requirements getting to working code was pretty quick.

Metalama isn't my focus - you can see much more beautiful code in the Metalama documentation - but here is my current code PointlessWaymarks.LlamaAspects.

@hez2010
Copy link

hez2010 commented Oct 11, 2024

It would be better to warn users on the old field usage as well and provide a code fix to help developers migrate from attributes on fields to attributes on partial properties after we ship the support.
The current way doesn't work well with COM/WinRT generators as all those properties are generated so that they cannot be seen by other source generators.

@Sergio0694
Copy link
Member Author

I plan on adding a code fixer for that as well, yes 🙂

@heartacker
Copy link

hello, when can we get this with dotnet 9 released?

@AndrewKeepCoding
Copy link

hello, when can we get this with dotnet 9 released?

  • Install CommunityToolkit.Mvvm v8.4.0-preview1.
    (Make sure you enabled the Include prerelease checkbox.)
  • In your *.csproj file, set the language version to preview.
    (field didn't make it to C#13 but is expected to be on C#14.)
    <PropertyGroup>
      <LangVersion>preview</LangVersion>
    </PropertyGroup>

@Mrxx99
Copy link

Mrxx99 commented Nov 15, 2024

When is a stable version released with this?

@Sergio0694 Sergio0694 unpinned this issue Dec 6, 2024
@weitzhandler
Copy link

Hi,

I've been using backed field properties for a while now and it works amazing, thank you!

There's one thing in particular I'd like to share.

Since there's no way to access the field from outside the property, a change notification is always going to be triggered, even when setting the property from the constructor.
This is totally redundant in most cases.

Is there a way to suppress INPC or set the backing field directly when setting the property in constructor?

@SirViver
Copy link

SirViver commented Jan 16, 2025

Actually, this is quite a big issue IMO.

The change to partial properties potentially causes breaking changes when converting an existing codebase, as there might be a ton of override OnPropertyChanged(...) or partial On{Property}Changed(...) that simply break behavior or cause exceptions when executed from within the constructor or property initializer.

Is there any way other than adding a bool _isInitialized field to every class that is set at the end of the ctor and then manually checked in each and every OnXXXChanged to avoid premature property change actions?

Maybe provide an protected Initialize(Action initializer) that internally suppresses any property changed notifications/calls, and the user has to change the ctor to something like this:

public class MyClass : ObservableObject
{
    [ObsevableProperty]
    public partial bool SomeProp { get; set; }

    public MyClass()
    {
        Initialize(() => 
        {
            SomeProp = true;
        });
    }
}

Not exactly sexy (and doesn't allow for property initializers at all), but I don't think there's really a native way to detect "derived ctor has finished executing" in C# ☹

I mean, ultimately it will simply not be possible to just convert everything to partial properties - sometimes you just need the possibility to change a property value without triggering change notifications - so living in a mixed mode world where some properties use fields and some use partial properties is probably unavoidable, but maybe there ought to be a bit more of a warning about the potential pitfalls when/before executing the suggested CodeFix conversion to partial properties.

Edit: Property initializers aren't a problem actually, as they directly set the backing field and don't use the property setter. Kinda obvious in hindsight, considering the existence of getter-only auto-properties.

@jphorv-bdo
Copy link

I also have unexpected issues with this partial property feature, and I'm unsure I'll be able to use it nearly as much as I'd hoped. Notifications being sent while still in the constructor causes a lot of issues in my codebase. What started with me happily deleting hand-written properties that existed only because I wanted private setters, ended with me stashing the entire changeset.

@SirViver
Copy link

Another (ugly/hack) workaround would be to make a SetPropertyWithoutNotification method that directly sets the property backing field via reflection.

// Custom ObservableObject-derived base class defines this method:

protected void SetPropertyWithoutNotification<T>(T property, T newValue,
    [CallerArgumentExpression(nameof(property))] string propertyName = "")
{
    if (propertyName.LastIndexOf('.') is int dotIndex and >= 0)
    {
        propertyName = propertyName[(dotIndex + 1)..];
    }

    string backingFieldName = $"<{propertyName}>k__BackingField";
    
    var backingField = this.GetType().GetField(backingFieldName, BindingFlags.Instance | BindingFlags.NonPublic) 
        ?? throw new InvalidOperationException($"Backing field '{backingFieldName}' not found for property '{propertyName}'.");
    
    backingField.SetValue(this, newValue);
}

// Use in derived class like so:

[ObservableProperty]
public partial int MyProperty { get; set; }

public MyClass()
{
    SetPropertyWithoutNotification(MyProperty, 42);
}

Of course the usual caveats regarding reflection performance and ignoring/circumvention of property access modifiers apply, but... 🤷

@Sergio0694
Copy link
Member Author

@weitzhandler @SirViver we're looking at fixing this in C# 14 🙂
See: dotnet/csharplang#9031.

@AignerGames
Copy link

AignerGames commented Jan 22, 2025

The fieldof thing looks weird, I think the easier and more obvious solution would be to give more control the the source generator. Instead of always using the field keyword for the generated property, add a parameter to the property attribute to generate a "real" field with a name. Then you could just write the value to the generated field like before, but still use the new features which are only possible with partial properties.

Would be a clean and easy solution, without waiting for a new language feature, which is in proposal state, so it's not confirmed that it will be added in C# 14.

@weitzhandler
Copy link

weitzhandler commented Jan 22, 2025

Another method is adding a IDisposable SuppressChangeNotifications() method in a fashion similar to what we have in the ObservableCollection - calling the method disables notification, disposing its result enables it back on.

class MyClass : INPC
{
    [ObservableProperty]
    partial int MyProperty { get; set; }

    public MyClass()
    {
        using (SuppressPropertyChangeNotifications())
        {
            MyProperty = 5;
        }
    }
}

This is a useful utility method which comes handy regardless of partial properties or initialization.

@DennisWelu
Copy link

@Sergio0694 I appreciate the entire body of work done in this library it is quite nice. But the philosophy of waiting for the next C# version continues to undermine the usefulness of the ObservableProperty, which may be the most used feature. I am hoping you will reconsider a solution for "today" in the meantime.

@JochemPalmsens
Copy link

@Sergio0694 I appreciate the entire body of work done in this library it is quite nice. But the philosophy of waiting for the next C# version continues to undermine the usefulness of the ObservableProperty, which may be the most used feature. I am hoping you will reconsider a solution for "today" in the meantime.

IMHO If you think this is a problem, don't use it. Make your own: you can even clone this repo and modify it yourself.
Or just be glad that they're offering this toolkit at all, which saves us a lot of work and makes code more readible. And that they are actually reading and addressing our issues at al.
At least, that's how I'm experiencing it. I'm happy with this new feature and glad with the partial property support in .net 9, so we can make stuff required. Thanks guys!

@DennisWelu
Copy link

@JochemPalmsens I led with a statement of appreciation, which is genuine. And everything you say is valid. But it makes no sense to fork and maintain a large repo every time you disagree with a design or decision. And since this is a community project I assume it is open to community opinions. The history on waiting for a key change in the C# spec on this specific feature, the most valuable feature IMO, goes back years.

@AignerGames
Copy link

Another method is adding a IDisposable SuppressChangeNotifications() method in a fashion similar to what we have in the ObservableCollection - calling the method disables notification, disposing its result enables it back on.

class MyClass : INPC
{
[ObservableProperty]
partial int MyProperty { get; set; }

public MyClass()
{
    using (SuppressPropertyChangeNotifications())
    {
        MyProperty = 5;
    }
}

}

This is a useful utility method which comes handy regardless of partial properties or initialization.

I like this idea and it could even be combined with the other ideas, if desired.

@whiskhub
Copy link

whiskhub commented Mar 4, 2025

I have had created a ticket for this feature request already: #993

After discussions I have then created the C# language request, but it seems they will not add it to the language anytime soon as they don't see the justification for a language level fix: dotnet/csharplang#8704 (comment)

Therefore I'd also really like to see a feature improvement in the source generator. An IDisposable wrapper (#555 (comment)) seems sensible to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request 📬 A request for new changes to improve functionality mvvm-toolkit 🧰 Issues/PRs for the MVVM Toolkit
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.