diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCarouselViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCarouselViewPage.xaml
index 09329a403c..60f73cbab6 100644
--- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCarouselViewPage.xaml
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCarouselViewPage.xaml
@@ -18,7 +18,7 @@
Text="This page demonstrates that the MediaElement can be used inside of a DataTemplate"
Margin="12,0,12,0"/>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCollectionViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCollectionViewPage.xaml
index 931bb4fd30..194221278b 100644
--- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCollectionViewPage.xaml
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementCollectionViewPage.xaml
@@ -10,39 +10,39 @@
Padding="0, 20, 0, 0"
Title="MediaElement in CollectionView">
-
-
-
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj b/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj
index 63a476189c..e87ba03235 100644
--- a/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj
+++ b/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj
@@ -47,13 +47,19 @@
Debug;Release
$(WarningsAsErrors);CS1591
True
+ true
+ $(BaseIntermediateOutputPath)\GeneratedFiles
-
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs b/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs
index 41334f086c..7a87bba749 100644
--- a/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs
@@ -9,20 +9,15 @@ namespace CommunityToolkit.Maui.Core;
public interface IMediaElement : IView, IAsynchronousMediaElementHandler
{
///
- /// Gets or sets the title of the media.
- ///
- string MetadataTitle { get; set; }
-
- ///
- /// Gets or sets the artist of the media.
+ /// Occurs when changed.
///
- string MetadataArtist { get; set; }
+ event EventHandler StateChanged;
///
- /// Gets or sets the artwork Image Url.
+ /// Occurs when the changes;
///
- string MetadataArtworkUrl { get; set; }
-
+ event EventHandler PositionChanged;
+
///
/// Gets the media aspect ratio.
///
@@ -98,18 +93,23 @@ public interface IMediaElement : IView, IAsynchronousMediaElementHandler
///
/// Gets or sets the volume of the audio for the media.
///
- /// A value of 1 means full volume, 0 is silence.
+ /// A value of 1 indicates full volume, 0 is silence.
double Volume { get; set; }
+
+ ///
+ /// Gets or sets the title of the media.
+ ///
+ string MetadataTitle { get; set; }
///
- /// Occurs when changed.
+ /// Gets or sets the artist of the media.
///
- event EventHandler StateChanged;
+ string MetadataArtist { get; set; }
///
- /// Occurs when the changes;
+ /// Gets or sets the artwork Image Url.
///
- event EventHandler PositionChanged;
+ string MetadataArtworkUrl { get; set; }
///
/// Occurs when the media has ended playing successfully.
diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
index 957bdfd78f..bdf77170f1 100644
--- a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
@@ -1,9 +1,6 @@
using System.ComponentModel;
using CommunityToolkit.Maui.Converters;
using CommunityToolkit.Maui.Core;
-using Microsoft.Maui;
-using Microsoft.Maui.Controls;
-using Microsoft.Maui.Dispatching;
namespace CommunityToolkit.Maui.Views;
@@ -12,116 +9,6 @@ namespace CommunityToolkit.Maui.Views;
///
public partial class MediaElement : View, IMediaElement, IDisposable
{
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty AspectProperty =
- BindableProperty.Create(nameof(Aspect), typeof(Aspect), typeof(MediaElement), Aspect.AspectFit);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty CurrentStateProperty =
- BindableProperty.Create(nameof(CurrentState), typeof(MediaElementState), typeof(MediaElement),
- MediaElementState.None, propertyChanged: OnCurrentStatePropertyChanged);
-
- static readonly BindablePropertyKey durationPropertyKey =
- BindableProperty.CreateReadOnly(nameof(Duration), typeof(TimeSpan), typeof(MediaElement), TimeSpan.Zero);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty DurationProperty = durationPropertyKey.BindableProperty;
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty ShouldAutoPlayProperty =
- BindableProperty.Create(nameof(ShouldAutoPlay), typeof(bool), typeof(MediaElement), false);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty ShouldLoopPlaybackProperty =
- BindableProperty.Create(nameof(ShouldLoopPlayback), typeof(bool), typeof(MediaElement), false);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty ShouldKeepScreenOnProperty =
- BindableProperty.Create(nameof(ShouldKeepScreenOn), typeof(bool), typeof(MediaElement), false);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty ShouldMuteProperty =
- BindableProperty.Create(nameof(ShouldMute), typeof(bool), typeof(MediaElement), false);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty PositionProperty =
- BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(MediaElement), TimeSpan.Zero);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty ShowsPlaybackControlsProperty =
- BindableProperty.Create(nameof(ShouldShowPlaybackControls), typeof(bool), typeof(MediaElement), true);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty SourceProperty =
- BindableProperty.Create(nameof(Source), typeof(MediaSource), typeof(MediaElement),
- propertyChanging: OnSourcePropertyChanging, propertyChanged: OnSourcePropertyChanged);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty SpeedProperty =
- BindableProperty.Create(nameof(Speed), typeof(double), typeof(MediaElement), 1.0);
-
- static readonly BindablePropertyKey mediaHeightPropertyKey =
- BindableProperty.CreateReadOnly(nameof(MediaHeight), typeof(int), typeof(MediaElement), 0);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty MediaHeightProperty =
- mediaHeightPropertyKey.BindableProperty;
-
- static readonly BindablePropertyKey mediaWidthPropertyKey =
- BindableProperty.CreateReadOnly(nameof(MediaWidth), typeof(int), typeof(MediaElement), 0);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty MediaWidthProperty =
- mediaWidthPropertyKey.BindableProperty;
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty VolumeProperty =
- BindableProperty.Create(nameof(Volume), typeof(double), typeof(MediaElement), 1.0,
- BindingMode.TwoWay, propertyChanging: ValidateVolume);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty MetadataTitleProperty = BindableProperty.Create(nameof(MetadataTitle), typeof(string), typeof(MediaElement), string.Empty);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty MetadataArtistProperty = BindableProperty.Create(nameof(MetadataArtist), typeof(string), typeof(MediaElement), string.Empty);
-
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty MetadataArtworkUrlProperty = BindableProperty.Create(nameof(MetadataArtworkUrl), typeof(string), typeof(MediaElement), string.Empty);
-
readonly WeakEventManager eventManager = new();
readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1);
@@ -211,190 +98,117 @@ internal event EventHandler StopRequested
add => eventManager.AddEventHandler(value);
remove => eventManager.RemoveEventHandler(value);
}
-
+
///
- /// The current position of the playing media. This is a bindable property.
+ /// Gets the in pixels.
///
- public TimeSpan Position => (TimeSpan)GetValue(PositionProperty);
+ [BindableProperty(DefaultValue = MediaElementDefaults.MediaHeight)]
+ public partial int MediaHeight { get; }
///
- /// Gets total duration of the loaded media. This is a bindable property.
+ /// Gets the in pixels.
///
- /// Might not be available for some types, like live streams.
- public TimeSpan Duration => (TimeSpan)GetValue(DurationProperty);
-
+ [BindableProperty(DefaultValue = MediaElementDefaults.MediaWidth)]
+ public partial int MediaWidth { get; }
+
///
- /// Read the MediaElementOptions set in on construction, cannot be changed after construction
+ /// Gets the current of the media playback.
///
- public AndroidViewType AndroidViewType { get; init; } = MediaElementOptions.DefaultAndroidViewType;
-
+ [BindableProperty(DefaultValue = MediaElementDefaults.Position)]
+ public partial TimeSpan Position { get; }
+
///
- /// Gets or sets whether the media should start playing as soon as it's loaded.
- /// Default is . This is a bindable property.
+ /// Gets the of the media.
///
- public bool ShouldAutoPlay
- {
- get => (bool)GetValue(ShouldAutoPlayProperty);
- set => SetValue(ShouldAutoPlayProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.Duration)]
+ public partial TimeSpan Duration { get; }
///
- /// Gets or sets if the video plays when reaches the end.
- /// Default is . This is a bindable property.
+ /// Gets or sets the of the media.
///
- public bool ShouldLoopPlayback
- {
- get => (bool)GetValue(ShouldLoopPlaybackProperty);
- set => SetValue(ShouldLoopPlaybackProperty, value);
- }
+ public AndroidViewType AndroidViewType { get; init; } = MediaElementOptions.DefaultAndroidViewType;
///
- /// Gets or sets if media playback prevents the device display from going to sleep.
- /// This is a bindable property.
+ /// Gets or sets the ratio used to display the video content.
///
- /// If media is paused, stopped or has completed playing, the display will turn off.
- public bool ShouldKeepScreenOn
- {
- get => (bool)GetValue(ShouldKeepScreenOnProperty);
- set => SetValue(ShouldKeepScreenOnProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.Aspect)]
+ public partial Aspect Aspect { get; set; }
///
- /// Gets or sets if audio should be muted. This is a bindable property.
+ /// Gets or sets the indicating whether the media should play automatically.
///
- ///
- /// When is , changes to are ignored.
- /// The new volume will be applied when is set to again.
- /// When the user uses the platform player controls to influence the volume, it might still unmute.
- ///
- public bool ShouldMute
- {
- get => (bool)GetValue(ShouldMuteProperty);
- set => SetValue(ShouldMuteProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.ShouldAutoPlay)]
+ public partial bool ShouldAutoPlay { get; set; }
///
- /// Gets or sets whether the player should show the platform playback controls.
- /// This is a bindable property.
+ /// Gets or sets the indicating whether the media should loop playback.
///
- public bool ShouldShowPlaybackControls
- {
- get => (bool)GetValue(ShowsPlaybackControlsProperty);
- set => SetValue(ShowsPlaybackControlsProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.ShouldLoopPlayback)]
+ public partial bool ShouldLoopPlayback { get; set; }
///
- /// Gets or sets the source of the media to play.
- /// This is a bindable property.
+ /// Gets or sets the indicating whether the screen should be kept on during media playback.
///
- [TypeConverter(typeof(MediaSourceConverter))]
- public MediaSource? Source
- {
- get => (MediaSource)GetValue(SourceProperty);
- set => SetValue(SourceProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.ShouldKeepScreenOn)]
+ public partial bool ShouldKeepScreenOn { get; set; }
///
- /// Gets or sets the volume of the audio for the media.
+ /// Gets or sets the indicating whether the media should be muted.
///
- ///
- /// A value of 1 indicates full-volume, 0 is silence.
- /// When is , changes to are ignored.
- /// The new volume will be applied when is set to again.
- /// When the user uses the platform player controls to influence the volume, it might still unmute.
- ///
- public double Volume
- {
- get => (double)GetValue(VolumeProperty);
- set
- {
- switch (value)
- {
- case > 1:
- throw new ArgumentOutOfRangeException(nameof(value), value, $"The value of {nameof(Volume)} cannot be greater than {1}");
- case < 0:
- throw new ArgumentOutOfRangeException(nameof(value), value, $"The value of {nameof(Volume)} cannot be less than {0}");
- default:
- SetValue(VolumeProperty, value);
- break;
- }
- }
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.ShouldMute)]
+ public partial bool ShouldMute { get; set; }
///
- /// Gets or sets the speed with which the media should be played.
- /// This is a bindable property.
+ /// Gets or sets the indicating whether playback controls should be shown.
///
- /// A value of 1 means normal speed.
- /// Anything more than 1 is faster speed, anything less than 1 is slower speed.
- public double Speed
- {
- get => (double)GetValue(SpeedProperty);
- set => SetValue(SpeedProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.ShouldShowPlaybackControls)]
+ public partial bool ShouldShowPlaybackControls { get; set; }
///
- /// Gets the height (in pixels) of the loaded media in pixels.
- /// This is a bindable property.
+ /// Gets or sets the of the media.
///
- /// Not reported for non-visual media, sometimes not available for live-streamed content on iOS and macOS.
- public int MediaHeight => (int)GetValue(MediaHeightProperty);
+ [BindableProperty(PropertyChangedMethodName = nameof(OnSourcePropertyChanged), PropertyChangingMethodName = nameof(OnSourcePropertyChanging))]
+ [TypeConverter(typeof(MediaSourceConverter))]
+ public partial MediaSource? Source { get; set; }
///
- /// Gets the width (in pixels) of the loaded media in pixels.
- /// This is a bindable property.
+ /// Gets or sets the of the media playback.
///
- /// Not reported for non-visual media, sometimes not available for live-streamed content on iOS and macOS.
- public int MediaWidth => (int)GetValue(MediaWidthProperty);
+ [BindableProperty(DefaultValue = MediaElementDefaults.Speed)]
+ public partial double Speed { get; set; }
///
- /// Gets or sets the Title of the media.
- /// This is a bindable property.
+ /// Gets or sets the of the media.
///
- public string MetadataTitle
- {
- get => (string)GetValue(MetadataTitleProperty);
- set => SetValue(MetadataTitleProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.MetadataTitle)]
+ public partial string MetadataTitle { get; set; }
///
- /// Gets or sets the Artist of the media.
- /// This is a bindable property.
+ /// Gets or sets the of the media.
///
- public string MetadataArtist
- {
- get => (string)GetValue(MetadataArtistProperty);
- set => SetValue(MetadataArtistProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.MetadataArtist)]
+ public partial string MetadataArtist { get; set; }
///
- /// Gets or sets the Artwork Image Url of the media.
- /// This is a bindable property.
+ /// Gets or sets the of the media.
///
- public string MetadataArtworkUrl
- {
- get => (string)GetValue(MetadataArtworkUrlProperty);
- set => SetValue(MetadataArtworkUrlProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.MetadataArtworkUrl)]
+ public partial string MetadataArtworkUrl { get; set; }
///
- /// Gets or sets how the media will be scaled to fit the display area.
- /// Default value is . This is a bindable property.
+ /// Gets or sets the of the media.
///
- public Aspect Aspect
- {
- get => (Aspect)GetValue(AspectProperty);
- set => SetValue(AspectProperty, value);
- }
-
+ [BindableProperty(DefaultValue = MediaElementDefaults.Volume, DefaultBindingMode = BindingMode.TwoWay, PropertyChangingMethodName = nameof(OnVolumeChanging))]
+ public partial double Volume { get; set; }
+
///
- /// The current state of the . This is a bindable property.
+ /// Gets or sets the of the media.
///
- public MediaElementState CurrentState
- {
- get => (MediaElementState)GetValue(CurrentStateProperty);
- private set => SetValue(CurrentStateProperty, value);
- }
+ [BindableProperty(DefaultValue = MediaElementDefaults.CurrentState, PropertyChangedMethodName = nameof(OnCurrentStatePropertyChanged))]
+ public partial MediaElementState CurrentState { get; private set; }
+
+ ///
+ TaskCompletionSource IAsynchronousMediaElementHandler.SeekCompletedTCS => seekCompletedTaskCompletionSource;
TimeSpan IMediaElement.Position
{
@@ -405,7 +219,7 @@ TimeSpan IMediaElement.Position
if (currentValue != value)
{
- SetValue(PositionProperty, value);
+ SetValue(positionPropertyKey, value);
OnPositionChanged(new(value));
}
}
@@ -429,9 +243,6 @@ int IMediaElement.MediaHeight
set => SetValue(mediaHeightPropertyKey, value);
}
- ///
- TaskCompletionSource IAsynchronousMediaElementHandler.SeekCompletedTCS => seekCompletedTaskCompletionSource;
-
///
public void Dispose()
{
@@ -527,11 +338,30 @@ protected virtual void Dispose(bool disposing)
isDisposed = true;
}
- static void OnSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue) =>
- ((MediaElement)bindable).OnSourcePropertyChanged((MediaSource?)newValue);
+ static void OnSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var mediaElement = (MediaElement)bindable;
+ var source = (MediaSource?)newValue;
+
+ mediaElement.ClearTimer();
+
+ if (source is not null)
+ {
+ source.SourceChanged += mediaElement.OnSourceChanged;
+ SetInheritedBindingContext(source, mediaElement.BindingContext);
+ }
+
+ mediaElement.InvalidateMeasure();
+ mediaElement.InitializeTimer();
+ }
+
+ static void OnSourcePropertyChanging(BindableObject bindable, object oldValue, object newValue)
+ {
+ var mediaElement = (MediaElement)bindable;
+ var oldMediaSource = (MediaSource?)oldValue;
- static void OnSourcePropertyChanging(BindableObject bindable, object oldValue, object newValue) =>
- ((MediaElement)bindable).OnSourcePropertyChanging((MediaSource?)oldValue);
+ oldMediaSource?.SourceChanged -= mediaElement.OnSourceChanged;
+ }
static void OnCurrentStatePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
@@ -542,7 +372,7 @@ static void OnCurrentStatePropertyChanged(BindableObject bindable, object oldVal
mediaElement.OnStateChanged(new MediaStateChangedEventArgs(previousState, newState));
}
- static void ValidateVolume(BindableObject bindable, object oldValue, object newValue)
+ static void OnVolumeChanging(BindableObject bindable, object oldValue, object newValue)
{
var updatedVolume = (double)newValue;
@@ -551,6 +381,16 @@ static void ValidateVolume(BindableObject bindable, object oldValue, object newV
throw new ArgumentOutOfRangeException(nameof(newValue), $"{nameof(Volume)} can not be less than 0.0 or greater than 1.0");
}
}
+
+ void IMediaElement.MediaEnded() => OnMediaEnded();
+
+ void IMediaElement.MediaFailed(MediaFailedEventArgs args) => OnMediaFailed(args);
+
+ void IMediaElement.MediaOpened() => OnMediaOpened();
+
+ void IMediaElement.SeekCompleted() => OnSeekCompleted();
+
+ void IMediaElement.CurrentStateChanged(MediaElementState newState) => CurrentState = newState;
void OnTimerTick(object? sender, EventArgs e)
{
@@ -590,52 +430,6 @@ void OnSourceChanged(object? sender, EventArgs eventArgs)
InvalidateMeasure();
}
- void OnSourcePropertyChanged(MediaSource? newValue)
- {
- ClearTimer();
-
- if (newValue is not null)
- {
- newValue.SourceChanged += OnSourceChanged;
- SetInheritedBindingContext(newValue, BindingContext);
- }
-
- InvalidateMeasure();
- InitializeTimer();
- }
-
- void OnSourcePropertyChanging(MediaSource? oldValue)
- {
- if (oldValue is null)
- {
- return;
- }
-
- oldValue.SourceChanged -= OnSourceChanged;
- }
-
- void IMediaElement.MediaEnded()
- {
- OnMediaEnded();
- }
-
- void IMediaElement.MediaFailed(MediaFailedEventArgs args)
- {
- OnMediaFailed(args);
- }
-
- void IMediaElement.MediaOpened()
- {
- OnMediaOpened();
- }
-
- void IMediaElement.SeekCompleted()
- {
- OnSeekCompleted();
- }
-
- void IMediaElement.CurrentStateChanged(MediaElementState newState) => CurrentState = newState;
-
void OnPositionChanged(MediaPositionChangedEventArgs mediaPositionChangedEventArgs) =>
eventManager.HandleEvent(this, mediaPositionChangedEventArgs, nameof(PositionChanged));
diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaSource/FileMediaSource.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaSource/FileMediaSource.shared.cs
index 8c803481be..eff2d406b1 100644
--- a/src/CommunityToolkit.Maui.MediaElement/MediaSource/FileMediaSource.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/MediaSource/FileMediaSource.shared.cs
@@ -10,12 +10,6 @@ namespace CommunityToolkit.Maui.Views;
[TypeConverter(typeof(FileMediaSourceConverter))]
public sealed partial class FileMediaSource : MediaSource
{
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty PathProperty
- = BindableProperty.Create(nameof(Path), typeof(string), typeof(FileMediaSource), propertyChanged: OnFileMediaSourceChanged);
-
///
/// An implicit operator to convert a string value into a .
///
@@ -27,17 +21,14 @@ public static readonly BindableProperty PathProperty
///
/// A instance to convert to a string value.
public static implicit operator string?(FileMediaSource? fileMediaSource) => fileMediaSource?.Path;
-
+
///
/// Gets or sets the full path to the local file to use as a media source.
/// This is a bindable property.
///
- public string? Path
- {
- get => (string?)GetValue(PathProperty);
- set => SetValue(PathProperty, value);
- }
-
+ [BindableProperty(PropertyChangedMethodName = nameof(OnFileMediaSourceChanged))]
+ public partial string? Path { get; set; }
+
///
public override string ToString() => $"File: {Path}";
diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaSource/ResourceMediaSource.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaSource/ResourceMediaSource.shared.cs
index 69af77baf4..b2b79e7a25 100644
--- a/src/CommunityToolkit.Maui.MediaElement/MediaSource/ResourceMediaSource.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/MediaSource/ResourceMediaSource.shared.cs
@@ -10,12 +10,6 @@ namespace CommunityToolkit.Maui.Views;
[TypeConverter(typeof(FileMediaSourceConverter))]
public sealed partial class ResourceMediaSource : MediaSource
{
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty PathProperty
- = BindableProperty.Create(nameof(Path), typeof(string), typeof(ResourceMediaSource), propertyChanged: OnResourceMediaSourceMediaSourceChanged);
-
///
/// An implicit operator to convert a string value into a .
///
@@ -27,7 +21,7 @@ public static readonly BindableProperty PathProperty
///
/// A instance to convert to a string value.
public static implicit operator string?(ResourceMediaSource? resourceMediaSource) => resourceMediaSource?.Path;
-
+
///
/// Gets or sets the full path to the resource file to use as a media source.
/// This is a bindable property.
@@ -36,12 +30,9 @@ public static readonly BindableProperty PathProperty
/// Path is relative to the application's resources folder.
/// It can only be just a filename if the resource file is in the root of the resources folder.
///
- public string? Path
- {
- get => (string?)GetValue(PathProperty);
- set => SetValue(PathProperty, value);
- }
-
+ [BindableProperty(PropertyChangedMethodName = nameof(OnResourceMediaSourceMediaSourceChanged))]
+ public partial string? Path { get; set; }
+
///
public override string ToString() => $"Resource: {Path}";
diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaSource/UriMediaSource.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaSource/UriMediaSource.shared.cs
index a7c4143069..11192b3c0e 100644
--- a/src/CommunityToolkit.Maui.MediaElement/MediaSource/UriMediaSource.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/MediaSource/UriMediaSource.shared.cs
@@ -9,12 +9,6 @@ namespace CommunityToolkit.Maui.Views;
///
public sealed partial class UriMediaSource : MediaSource
{
- ///
- /// Backing store for the property.
- ///
- public static readonly BindableProperty UriProperty =
- BindableProperty.Create(nameof(Uri), typeof(Uri), typeof(UriMediaSource), propertyChanged: OnUriSourceChanged, validateValue: UriValueValidator);
-
///
/// An implicit operator to convert a string value into a .
///
@@ -26,19 +20,16 @@ public sealed partial class UriMediaSource : MediaSource
///
/// A instance to convert to a string value.
public static implicit operator string?(UriMediaSource? uriMediaSource) => uriMediaSource?.Uri?.ToString();
-
+
///
/// Gets or sets the URI to use as a media source.
/// This is a bindable property.
///
/// The URI has to be absolute.
[TypeConverter(typeof(UriTypeConverter))]
- public Uri? Uri
- {
- get => (Uri?)GetValue(UriProperty);
- set => SetValue(UriProperty, value);
- }
-
+ [BindableProperty(PropertyChangedMethodName = nameof(OnUriSourceChanged), ValidateValueMethodName = nameof(UriValueValidator))]
+ public partial Uri? Uri { get; set; }
+
///
public override string ToString() => $"Uri: {Uri}";
diff --git a/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs b/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs
new file mode 100644
index 0000000000..4c02d95470
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs
@@ -0,0 +1,36 @@
+namespace CommunityToolkit.Maui.Core;
+
+static class MediaElementDefaults
+{
+ public const Aspect Aspect = Microsoft.Maui.Aspect.AspectFit;
+
+ public const int MediaHeight = 0;
+
+ public const int MediaWidth = 0;
+
+ public const string Position = "00:00:00";
+
+ public const string Duration = "00:00:00";
+
+ public const bool ShouldAutoPlay = false;
+
+ public const bool ShouldLoopPlayback = false;
+
+ public const bool ShouldKeepScreenOn = false;
+
+ public const bool ShouldMute = false;
+
+ public const bool ShouldShowPlaybackControls = false;
+
+ public const double Speed = 1.0;
+
+ public const double Volume = 1.0;
+
+ public const string MetadataTitle = "";
+
+ public const string MetadataArtist = "";
+
+ public const string MetadataArtworkUrl = "";
+
+ public const MediaElementState CurrentState = MediaElementState.None;
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs
index eaab309bc2..03dad02d1f 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs
@@ -264,7 +264,7 @@ public partial class {{defaultTestClassName}}
///
/// Backing BindableProperty for the property.
///
- public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (int)42, (Microsoft.Maui.Controls.BindingMode)Microsoft.Maui.Controls.BindingMode.TwoWay, ValidateValue, OnPropertyChanged, OnPropertyChanging, CoerceValue, CreateDefaultValue);
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (int)42, (Microsoft.Maui.Controls.BindingMode)1, ValidateValue, OnPropertyChanged, OnPropertyChanging, CoerceValue, CreateDefaultValue);
public partial int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
}
""";
@@ -351,4 +351,305 @@ public partial class {{defaultTestClassName}} : View
await VerifySourceGeneratorAsync(source, string.Empty);
}
+
+ [Fact]
+ public async Task GenerateBindableProperty_InternalSetter_GeneratesInternalSetter()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute]
+ public partial string Text { get; internal set; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public partial string Text { get => (string)GetValue(TextProperty); internal set => SetValue(TextProperty, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_PrivateProtectedSetter_GeneratesPrivateProtectedSetter()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute]
+ public partial string Text { get; private protected set; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty;
+ public partial string Text { get => (string)GetValue(TextProperty); private protected set => SetValue(textPropertyKey, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_ProtectedInternalSetter_GeneratesPrivateProtectedSetter()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute]
+ public partial string Text { get; protected internal set; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public partial string Text { get => (string)GetValue(TextProperty); protected internal set => SetValue(TextProperty, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_ProtectedSetter_GeneratesProtectedSetter()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute]
+ public partial string Text { get; protected set; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty;
+ public partial string Text { get => (string)GetValue(TextProperty); protected set => SetValue(textPropertyKey, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_PrivateSetter_GeneratesPrivateSetter()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute]
+ public partial string Text { get; private set; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty;
+ public partial string Text { get => (string)GetValue(TextProperty); private set => SetValue(textPropertyKey, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_GetOnly_GeneratesGetterOnly()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute]
+ public partial string Text { get; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty;
+ public partial string Text { get => (string)GetValue(TextProperty); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_WithTimeSpanStringDefaultValue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+ using System;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindablePropertyAttribute(DefaultValue = "00:00:00")]
+ public partial TimeSpan Position { get; set; }
+
+ [BindablePropertyAttribute(DefaultValue = "00:01:30")]
+ public partial TimeSpan CustomDuration { get; set; }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty PositionProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Position", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.TimeSpan.Zero, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public partial System.TimeSpan Position { get => (System.TimeSpan)GetValue(PositionProperty); set => SetValue(PositionProperty, value); }
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), new global::System.TimeSpan(900000000), Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public partial System.TimeSpan CustomDuration { get => (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs
index 8391d60fc2..ae67355250 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs
@@ -53,6 +53,102 @@ public partial class {{defaultTestClassName}}
await VerifySourceGeneratorAsync(source, expectedGenerated);
}
+
+ [Fact]
+ public async Task GenerateBindableProperty_PropertyIsByteEnum_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindableProperty(DefaultValue = Status.Approved)]
+ public partial Status InvoiceStatus { get; set; }
+ }
+
+ public enum Status : byte
+ {
+ Pending = 0,
+ Approved = 1,
+ Rejected = 2
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (TestNamespace.Status)1, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public partial TestNamespace.Status InvoiceStatus { get => (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateBindableProperty_PropertyIsLongEnum_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ using CommunityToolkit.Maui;
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public partial class {{defaultTestClassName}} : View
+ {
+ [BindableProperty(DefaultValue = Status.Rejected)]
+ public partial Status InvoiceStatus { get; set; }
+ }
+
+ public enum Status : long
+ {
+ Pending = 0,
+ Approved = 1,
+ Rejected = long.MaxValue
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ //lang=csharp
+ $$"""
+ //
+ // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
+ #pragma warning disable
+ #nullable enable
+ namespace {{defaultTestNamespace}};
+ public partial class {{defaultTestClassName}}
+ {
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (TestNamespace.Status)9223372036854775807, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
+ public partial TestNamespace.Status InvoiceStatus { get => (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
[Fact]
public async Task GenerateBindableProperty_NullableValueTypes_GeneratesCorrectCode()
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs
index 2f0946f120..7738db8a20 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs
@@ -27,7 +27,10 @@ public void BindablePropertyName_ReturnsCorrectPropertyName()
"null",
"null",
"null",
- string.Empty);
+ string.Empty,
+ true, // IsReadOnlyBindableProperty
+ string.Empty // SetterAccessibility
+ );
// Act
var bindablePropertyName = model.BindablePropertyName;
@@ -66,7 +69,10 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
propertyChangingMethodName,
coerceValueMethodName,
defaultValueCreatorMethodName,
- newKeywordText);
+ newKeywordText,
+ true, // IsReadOnlyBindableProperty
+ string.Empty // SetterAccessibility
+ );
// Assert
Assert.Equal(propertyName, model.PropertyName);
@@ -120,7 +126,10 @@ public void SemanticValues_WithClassInfoAndProperties_StoresCorrectValues()
"null",
"null",
"null",
- string.Empty);
+ string.Empty,
+ true, // IsReadOnlyBindableProperty
+ string.Empty // SetterAccessibilityText
+ );
var bindableProperties = new[] { bindableProperty }.ToImmutableArray();
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
index 6bb28b63a6..7688ca3ceb 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
@@ -81,7 +81,7 @@ static void ExecuteAllValues(SourceProductionContext context, ImmutableArray();
+ list = [];
groupedValues[key] = list;
}
list.Add(sv);
@@ -174,7 +174,15 @@ static string GenerateSource(SemanticValues value)
foreach (var info in value.BindableProperties)
{
- GenerateBindableProperty(sb, in info);
+ if (info.IsReadOnlyBindableProperty)
+ {
+ GenerateReadOnlyBindableProperty(sb, in info);
+ }
+ else
+ {
+ GenerateBindableProperty(sb, in info);
+ }
+
GenerateProperty(sb, in info);
}
@@ -193,6 +201,58 @@ static string GenerateSource(SemanticValues value)
return sb.ToString();
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindablePropertyModel info)
+ {
+ // Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator
+ var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType);
+ var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;
+
+ sb.Append("/// \r\n/// Backing BindableProperty for the property.\r\n/// \r\n");
+
+ // Generate BindablePropertyKey for read-only properties
+ sb.Append("static readonly global::Microsoft.Maui.Controls.BindablePropertyKey ")
+ .Append(info.BindablePropertyKeyName)
+ .Append(" = \n")
+ .Append(bpFullName)
+ .Append(".CreateReadOnly(\"")
+ .Append(sanitizedPropertyName)
+ .Append("\", typeof(")
+ .Append(GetFormattedReturnType(nonNullableReturnType))
+ .Append("), typeof(")
+ .Append(info.DeclaringType)
+ .Append("), ")
+ .Append(info.DefaultValue)
+ .Append(", ")
+ .Append(info.DefaultBindingMode)
+ .Append(", ")
+ .Append(info.ValidateValueMethodName)
+ .Append(", ")
+ .Append(info.PropertyChangedMethodName)
+ .Append(", ")
+ .Append(info.PropertyChangingMethodName)
+ .Append(", ")
+ .Append(info.CoerceValueMethodName)
+ .Append(", ")
+ .Append(info.DefaultValueCreatorMethodName)
+ .Append(");\n");
+
+ // Generate public BindableProperty from the key
+ sb.Append("public ")
+ .Append(info.NewKeywordText)
+ .Append("static readonly ")
+ .Append(bpFullName)
+ .Append(' ')
+ .Append(info.BindablePropertyName)
+ .Append(" = ")
+ .Append(info.BindablePropertyKeyName)
+ .Append(".BindableProperty;\n");
+
+ sb.Append('\n');
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel info)
{
@@ -204,6 +264,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel
.Append(sanitizedPropertyName)
.Append("\"/> property.\r\n/// \r\n");
+ // Generate regular BindableProperty
sb.Append("public ")
.Append(info.NewKeywordText)
.Append("static readonly ")
@@ -232,7 +293,9 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel
.Append(info.CoerceValueMethodName)
.Append(", ")
.Append(info.DefaultValueCreatorMethodName)
- .Append(");\n\n");
+ .Append(");\n");
+
+ sb.Append('\n');
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -251,9 +314,18 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info)
.Append(formattedReturnType)
.Append(")GetValue(")
.Append(info.BindablePropertyName)
- .Append(");\nset => SetValue(")
- .Append(info.BindablePropertyName)
- .Append(", value);\n}\n");
+ .Append(");\n");
+
+ if (info.SetterAccessibility is not null)
+ {
+ sb.Append(info.SetterAccessibility)
+ .Append("set => SetValue(")
+ .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName)
+ .Append(", value);\n");
+ }
+ // else Do not create a Setter because the property is read-only
+
+ sb.Append("}\n");
}
static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
@@ -284,9 +356,10 @@ static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context,
var bindablePropertyModels = new BindablePropertyModel[context.Attributes.Length];
var doesContainNewKeyword = HasNewKeyword(propertyDeclarationSyntax);
+ var (isReadOnlyBindableProperty, setterAccessibility) = GetPropertyAccessibility(propertySymbol, propertyDeclarationSyntax);
var attributeData = context.Attributes[0];
- bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword);
+ bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility);
return new(propertyInfo, ImmutableArray.Create(bindablePropertyModels));
}
@@ -304,9 +377,30 @@ static bool HasNewKeyword(PropertyDeclarationSyntax syntax)
return false;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static (bool IsReadOnlyBindableProperty, string? SetterAccessibility) GetPropertyAccessibility(IPropertySymbol propertySymbol, PropertyDeclarationSyntax syntax)
+ {
+ // Check if property is get-only (no setter)
+ if (propertySymbol.SetMethod is null)
+ {
+ return (true, null);
+ }
+
+ return propertySymbol.SetMethod.DeclaredAccessibility switch
+ {
+ Accessibility.NotApplicable => throw new NotSupportedException($"The setter type for {propertySymbol.Name} is not yet supported"),
+ Accessibility.Private => (true, "private "),
+ Accessibility.ProtectedAndInternal => (true, "private protected "),
+ Accessibility.Protected => (true, "protected "),
+ Accessibility.Internal => (false, "internal "),
+ Accessibility.ProtectedOrInternal => (false, "protected internal "),
+ Accessibility.Public => (false, " "), // Keep the SetterAccessibility empty because the Property is public and the setter will inherit that accessbility modified, e.g. `public string Test { get; set; }`
+ _ => throw new NotSupportedException($"The setter type for {propertySymbol.Name} is not yet supported"),
+ };
+ }
+
static string GetContainingTypes(INamedTypeSymbol typeSymbol)
{
- // Use StringBuilder for efficient string building
var current = typeSymbol.ContainingType;
if (current is null)
{
@@ -362,23 +456,23 @@ static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol)
return sb.ToString();
}
- static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword)
+ static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility)
{
if (attributeData.AttributeClass is null)
{
- throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass));
+ throw new ArgumentException($"{nameof(attributeData)}.{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData));
}
- var defaultValue = attributeData.GetNamedTypeArgumentsAttributeValueByNameAsCastedString(nameof(BindablePropertyModel.DefaultValue));
+ var defaultValue = attributeData.GetNamedTypeArgumentsAttributeValueByNameAsCastedString(nameof(BindablePropertyModel.DefaultValue), returnType);
var coerceValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName));
- var defaultBindingMode = attributeData.GetNamedTypeArgumentsAttributeValueByNameAsCastedString(nameof(BindablePropertyModel.DefaultBindingMode), "Microsoft.Maui.Controls.BindingMode.OneWay");
+ var defaultBindingMode = attributeData.GetNamedTypeArgumentsAttributeValueForDefaultBindingMode(nameof(BindablePropertyModel.DefaultBindingMode), "Microsoft.Maui.Controls.BindingMode.OneWay");
var defaultValueCreatorMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName));
var propertyChangedMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName));
var propertyChangingMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName));
var validateValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));
var newKeywordText = doesContainNewKeyword ? "new " : string.Empty;
- return new BindablePropertyModel(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText);
+ return new BindablePropertyModel(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs
index 1cec709251..a3dd55c558 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs
@@ -1,6 +1,5 @@
-using System.Reflection;
+using System.Globalization;
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
namespace CommunityToolkit.Maui.SourceGenerators.Internal.Helpers;
@@ -12,7 +11,14 @@ public static TypedConstant GetAttributeValueByName(this AttributeData attribute
return x;
}
- public static string GetNamedTypeArgumentsAttributeValueByNameAsCastedString(this AttributeData attribute, string name, string placeholder = "null")
+ public static string GetNamedTypeArgumentsAttributeValueForDefaultBindingMode(this AttributeData attribute, string name, string placeholder = "null")
+ {
+ var data = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value;
+
+ return data.Value is null ? placeholder : $"({data.Type}){data.Value}";
+ }
+
+ public static string GetNamedTypeArgumentsAttributeValueByNameAsCastedString(this AttributeData attribute, string name, ITypeSymbol propertyType, string placeholder = "null")
{
var data = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value;
@@ -24,13 +30,24 @@ public static string GetNamedTypeArgumentsAttributeValueByNameAsCastedString(thi
if (data.Kind is TypedConstantKind.Enum && data.Type is not null && data.Value is not null)
{
- var members = data.Type.GetMembers();
-
- return $"({data.Type}){members[(int)data.Value]}";
+ return $"({data.Type}){data.Value}";
}
if (data.Type?.SpecialType is SpecialType.System_String)
{
+ // Special handling for TimeSpan string representations - only when property type is TimeSpan
+ if (data.Value is string stringValue && IsTimeSpanType(propertyType) && TimeSpan.TryParse(stringValue, CultureInfo.InvariantCulture, out var timeSpanValue))
+ {
+ // Check if it's TimeSpan.Zero
+ if (timeSpanValue == TimeSpan.Zero)
+ {
+ return "global::System.TimeSpan.Zero";
+ }
+
+ // For other TimeSpan values, use the ticks constructor
+ return $"new global::System.TimeSpan({timeSpanValue.Ticks})";
+ }
+
return data.Value is null ? $"\"{placeholder}\"" : $"({data.Type})\"{data.Value}\"";
}
@@ -48,4 +65,16 @@ public static string GetNamedMethodGroupArgumentsAttributeValueByNameAsString(th
return data.Value is null ? placeholder : data.Value.ToString();
}
+
+ static bool IsTimeSpanType(ITypeSymbol typeSymbol)
+ {
+ if (typeSymbol is null)
+ {
+ return false;
+ }
+
+ // Check if it's System.TimeSpan by comparing name
+ return typeSymbol is { Name: "TimeSpan", ContainingNamespace: not null }
+ && typeSymbol.ContainingNamespace.ToDisplayString() == "System";
+ }
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs
index 55b4b162a1..704e1aea28 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs
@@ -3,9 +3,10 @@
namespace CommunityToolkit.Maui.SourceGenerators.Internal.Models;
-record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultValue, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText)
+record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultValue, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility)
{
public string BindablePropertyName => $"{PropertyName}Property";
+ public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey";
}
record SemanticValues(ClassInformation ClassInformation, EquatableArray BindableProperties);
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs
index 3de87e3d2f..fd79166ef6 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/CameraView/CameraProviderTests.cs
@@ -2,7 +2,7 @@
using CommunityToolkit.Maui.UnitTests.Mocks;
using Xunit;
-namespace CommunityToolkit.Maui.UnitTests;
+namespace CommunityToolkit.Maui.UnitTests.Views;
///
/// Comprehensive unit tests for CameraProvider.AreCameraInfoListsEqual method
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs
index 2837c6fd67..510f339d16 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs
@@ -12,6 +12,33 @@ public MediaElementTests()
Assert.IsType(new MediaElement(), exactMatch: false);
}
+ [Fact]
+ public void VerifyDefaults()
+ {
+ // Arrange
+ MediaElement mediaElement = new();
+
+ // Act
+
+ // Assert
+ Assert.Equal(MediaElementDefaults.MediaHeight, mediaElement.MediaHeight);
+ Assert.Equal(MediaElementDefaults.Aspect, mediaElement.Aspect);
+ Assert.Equal(MediaElementDefaults.CurrentState, mediaElement.CurrentState);
+ Assert.Equal(TimeSpan.Parse(MediaElementDefaults.Duration), mediaElement.Duration);
+ Assert.Equal(MediaElementDefaults.MediaWidth, mediaElement.MediaWidth);
+ Assert.Equal(MediaElementDefaults.MetadataArtist, mediaElement.MetadataArtist);
+ Assert.Equal(MediaElementDefaults.MetadataArtworkUrl, mediaElement.MetadataArtworkUrl);
+ Assert.Equal(TimeSpan.Parse(MediaElementDefaults.Position), mediaElement.Position);
+ Assert.Equal(MediaElementDefaults.ShouldAutoPlay, mediaElement.ShouldAutoPlay);
+ Assert.Equal(MediaElementDefaults.ShouldKeepScreenOn, mediaElement.ShouldKeepScreenOn);
+ Assert.Equal(MediaElementDefaults.ShouldLoopPlayback, mediaElement.ShouldLoopPlayback);
+ Assert.Equal(MediaElementDefaults.ShouldMute, mediaElement.ShouldMute);
+ Assert.Equal(MediaElementDefaults.ShouldShowPlaybackControls, mediaElement.ShouldShowPlaybackControls);
+ Assert.Equal(MediaElementDefaults.Speed, mediaElement.Speed);
+ Assert.Equal(MediaElementDefaults.Volume, mediaElement.Volume);
+ Assert.Equal(MediaElementDefaults.MetadataTitle, mediaElement.MetadataTitle);
+ }
+
[Fact]
public void PosterIsNotStringEmptyOrNull()
{