MVVM "A 'Binding' can only be set on a DependencyProperty of a DependencyObject." - mvvm

I have this Model
[NotifyPropertyChanged]
public class WidgetConfiguration
{
#region Properties
#endregion Properties
}
Which i use in my ViewModel for a Collection and a Selected item property (ListView / GridView SelectedItem="{Binding SelectedWidget}" ... )
[NotifyPropertyChanged]
public class WidgetViewModel
{
public ObservableCollection<WidgetConfiguration> Configurations { get; set; } = new ObservableCollection<WidgetConfiguration>();
public WidgetConfiguration SelectedWidget { get; set; }
}
I then want to Bind SelectedWidget to a UserControl that function as editor for the SelectedItem:
<controls:WidgetConfig Widget="{Binding SelectedWidget}" />
The UserControl is defined like this (using PostSharp to declare DependencyProperties)
[NotifyPropertyChanged]
public partial class WidgetConfig : UserControl
{
[DependencyProperty]
public WidgetConfiguration Widget { get; set; }
public WidgetConfig()
{
InitializeComponent();
this.DataContext = this;
}
}
But im getting an error on the UserControl binding:
Severity Code Description Project File Line Suppression State Error A
'Binding' cannot be set on the 'Widget' property of type
'Squiddy_Client_Views_WidgetConfig_10_577403948'. A 'Binding' can only
be set on a DependencyProperty of a
DependencyObject. Client C:\develop\Squiddy\Client\Views\WidgetManager.xaml 21
I've tried implementing the DependencyProperties manually and ensured that all types was correct, even the default type and default value. it didn't help.
I've read all results on google and don't know what to do.
Is this even possible or do i need to make a proxy binding ?
EDIT:
Just for the sake of it, i tried implementing the DependencyProperty manually:
public static readonly DependencyProperty WidgetProperty =
DependencyProperty.Register("Widget", typeof(WidgetConfiguration),
typeof(WidgetConfig));
[SafeForDependencyAnalysis]
public WidgetConfiguration Widget
{
get { return GetValue(WidgetProperty) as WidgetConfiguration; }
set { SetValue(WidgetProperty, value); }
}
Now the XAML error is gone, but the binding is "dead". When selecting a new object in the ListView, the UserControl doesn't get updated:
The PropertySetter doesn't get invoked
PropertyChanged events on the ViewModel DO happen though...
EDIT 2:
I totally missed this part in the PostSharp documentation, i lacked adding the DependencyProperty along with the attribute. (thanks to Daniel Balas)
public static DependencyProperty WidgetProperty { get; private set; }
[DependencyProperty]
public WidgetConfiguration Widget { get; set; }
EDIT 3:
I finally found the answer to DataContext / root after watching this video:
https://www.youtube.com/watch?v=h7ZrdGiOm3E
I removed "this.DataContext = this" from the UserControl constructor
I added a Name="root" in the UserControl element in XAML
The bindings inside the UserControl should point to ElementName=root and use the property Widget.xxx
like this:
<UserControl Name="root">
<TextBlock Text="{Binding Header, ElementName=root}"></TextBlock>
<Label Content="{Binding Widget.Name, ElementName=root}" />
</UserControl>

The Xaml/Baml compiler determines whether the property Foo is a dependency property by looking for a FooProperty static field or property with DependencyProperty type. This field is not automatically injected by the [DependencyProperty] aspect (due to limitations of PostSharp's aspect framework).
However, when you declare this field or property it would be enough for the Xaml compiler to recognize the property as a dependency property. ( The aspect will then set the field/property at runtime, so it has a correct value at runtime and is usable. )
public static DependencyProperty WidgetProperty { get; private set; }
[DependencyProperty]
public WidgetConfiguration Widget { get; set; }
Your property setter is not invoked, because WPF bindings change the value store on the DependencyObject itself instead of accessing the property setter.
The problem seems to be in the fact that you are changing the DataContext of your control in the constructor. This is going to break Bindings set on the DataContext in the parent control (bindings use DataContext of controls they assigned to). One way to reference properties of your control is like this:
<Label x:Name="label" Content="{Binding ElementName=root, Path=Widget.Name}" HorizontalAlignment="Left" />
Where "root" is a x:Name="root" given to your control (the root UserControl element).

Related

ms mvvm toolkit: can't work out how to wire up ReplayCommand and canExecute

I have a simple model:
public sealed partial class ResultsModel : ObservableObject {
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(ClearCommand))]
[ObservableProperty]
ObservableCollection<Arrivals> _arrivals = new();
public RelayCommand SaveCommand { get; private set; }
public RelayCommand ClearCommand { get; private set; }
internal ResultsModel() {
SaveCommand = new RelayCommand(SaveRequest, CanSaveClear);
ClearCommand = new RelayCommand(OnClear, CanSaveClear);
}
public bool CanSaveClear() {
return _arrivals.Count > 0;
}
void OnClear() {
_arrivals.Clear();
}
async void SaveRequest() {
// save stuff
}
}
// c#
DataContext = (model = new ResultsModel());
...
model.Arrivals.insert(0, thing);
// The _arrivals are bound to an ItemsRepeater and appear in gui as //they're added
<ItemsRepeater ItemsSource="{Binding Arrivals}">
<Button Content="Clear" Command="{Binding ClearCommand}"/>
<Button Content="Save" Command="{Binding SaveCommand}" />
I've bound buttons to the two Commands and they work ok, I just can't work how to get the canExecute code to run more than one time.
I was expecting that when items get added to the _arrivals collection (and they do) the canExecutes would be re-evaluated via the NotifyCanExecuteChangedFor attribute, but I'm obviously missing some glue somewhere because the button are always disabled.
Any help would be appreciated.
It won't happen when you added an item to the Arrivals. But it will happen when you change the Arrivals by giving a new ObservableCollection. You could create a simple string property to test this behavior.
The reason for this behavior is that when the NotifyCanExecuteChangedFor Attribute is used, the IRelayCommand.NotifyCanExecuteChanged will be called when the setter of the property is called. In your scenario, that means only when the setter of the Arrivals property is called, this Attribute will call the IRelayCommand.NotifyCanExecuteChanged.

.Net Maui: How to read/write (get/set) a global object from any content page (MVVM)

I'm sure that I'm missing some deep or obvious concept here :)
So I have a page now that can setup various Bluetooth sensors and get data from a heart rate monitor, speedometer and cadence sensor. (Using Plugin.BLE)
So I do all of that in a ViewModel for a ContentPage called BluetoothPage.
I want to display the data I get in a different ContentPage called DisplayPage.
I have created a simple class (model) that can hold the data I want:
namespace TSDZ2Monitor.Models;
public partial class BluetoothData : ObservableObject
{
//Heart rate raw data
public int HRM { get; set; }
public double HRR { get; set; }
//SPD raw data
public int SPDWheelRevolutions { get; set; }
public double SPDWheelEventTime { get; set; }
//CAD raw data
public int CADCrankRevolutions { get; set; }
public double CADCrankEventTime { get; set; }
}
So, how do I get the data from my Bluetooth page to my Display page?
I suspect I need to use an object based on my model and populate it with data in my Bluetooth viewmodel (easy...ish)?
But how can my Display page see this data as it happens?
When I tried working with ReactNative this sort of thing was a nightmare (State!)
Or am I being a bit simple in the head here :lol
Workaround: I could save the data to some local storage or sqlite as per https://learn.microsoft.com/en-us/learn/dotnet-maui/store-local-data/2-compare-storage-options - is that the way to do it, or can it be done with the object?
G.
Edit: I think I could also use the MessagingService https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/messagingcenter and https://codemilltech.com/messing-with-xamarin-forms-messaging-center/ if I can figure out how to use them in MVVM context.
Also What is the difference between using MessagingCenter and standard .NET event handlers for informing interested parties of changes?
So it seems that using the MessagingCenter was a way to go.
Following the guidance in https://codemilltech.com/messing-with-xamarin-forms-messaging-center/
I created a MessagingMarker class:
namespace TSDZ2Monitor.Classes;
public class MessagingMarker
{
}
That's all.
In the ViewModel where I wanted to send an object from, I did
MessagingCenter.Send(new MessagingMarker(), "BTDataUpdate", btd);
where btd was an instance of a class I created to hold my data: Here is a simplified model:
namespace TSDZ2Monitor.Models;
public partial class BluetoothData : ObservableObject
{
//Heart rate raw data
private int hRM;
public int HRM //heart rate
{
get => hRM;
set => SetProperty(ref hRM, value);
}
private double hRR; //heartrate R-R value
public double HRR
{
get => hRR;
set => SetProperty(ref hRR, value);
}
private double wheelRPM;
public double WheelRPM
{
get => wheelRPM;
set => SetProperty(ref wheelRPM, value);
}
private double cadence;
public double Cadence
{
get => cadence;
set => SetProperty(ref cadence, value);
}
}
In the constructor of the ViewModel for the sending page (probably best somewhere else?)
public BluetoothData btd = new();
This is not used in the XAML for this ViewModel
In my receiving ViewModel
I also created an instance of the BluetoothData class, but this is used in the XAML bindings
[ObservableProperty]
private BluetoothData bTData;
and in the constructor of the ViewModel I had
BTData = new BluetoothData();
MessagingCenter.Subscribe<MessagingMarker, BluetoothData>(this, "BTDataUpdate", (sender, arg) =>
{
//Debug.WriteLine($"Message received {arg}");
BTData.HRM = arg.HRM;
BTData.HRR = arg.HRR;
BTData.WheelRPM = arg.WheelRPM;
BTData.Cadence = arg.Cadence;
});
Well it works, don't know what the impact on performance might be, but it seems pretty responsive.
To my way of thinking though a more idea solution is to create a global instance of any class that any ViewModel can access.
I had need to also do this -- in my case, reference an ObservableCollection<State> States from multiple pages. Sharing this as another possible solution:
I created a class with the ObservableCollection as a static member which is populated once upon first use:
public class Filter
{
...
public static ObservableCollection<State> StateSelections { get; } = new();
...
public Filter(DataService dataService) : base()
{
this.dataService = dataService;
PopulateData();
}
public async Task PopulateData()
{
// Populate the available selections
await LoadStates();
}
public async Task LoadStates()
{
if (StateSelections?.Count > 0)
return;
...
}
}
For each page that uses the collection, its VM has a reference to an instance of the class:
public partial class ParkListVM : BaseVM
{
...
public Filter Filter { get; set; }
...
public void PopulateData()
{
if (ParkListVM.Filter is null)
ParkListVM.Filter = new Filter(dataService);
...
}
}
And the page has a reference to the static collection for display:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:NationalParks.ViewModels"
xmlns:model="clr-namespace:NationalParks.Models"
x:DataType="vm:ParkListVM"
x:Class="NationalParks.Views.ParkListPage"
Title="{Binding Title}">
<ContentPage.Resources>
<model:Filter x:Key="Filter"/>
</ContentPage.Resources>
...
<CollectionView ItemsSource="{Binding Source={x:Static model:Filter.StateSelections}}"
SelectionMode="Multiple"
SelectedItems="{Binding SelectedStates}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:State">
<VerticalStackLayout>
<Label Style="{StaticResource LabelMedium}" Text="{Binding Name}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
...
</ContentPage>

How to add attributes to aspect/compiler-generated property?

I'm adding a property to a class and have my Aspect set up like this:
[PSerializable]
public class MyAspect : InstanceLevelAspect
{
[MaxLength(100)]
[IntroduceMember]
public string MyProp { get; set; }
}
My target class is then decorated with the [MyAspect] attribute.
When Postsharp adds the MyProp property to the target class, it does not include the MaxLength attribute on the generated property. The property in the Aspect class includes it, but the property added to my target class does not.
I need the attributes specified for the property in the aspect definition to be added to the property added to my target class. How can I add the attributes to the generated property?
To intruct PostSharp to copy the attribute, you need to use [CopyCustomAttributes] (doc), so your aspect would look like this:
[PSerializable]
public class MyAspect : InstanceLevelAspect
{
[MaxLength(100)]
[IntroduceMember]
[CopyCustomAttributes(typeof(MaxLengthAttribute))]
public string MyProp { get; set; }
}

Xamarin Forms Custom Bindable Command type mismatch

I've created a custom view called PINControl, which shows a PIN entry with a configurable amount of digits.
The XAML I'd like to use in my ContentPage is
<local:PINControl x:Name="PIN"
PINLength="5"
PINCompleteCommand="{Binding CompletePIN}"
HorizontalOptions="CenterAndExpand" />
My BindableProperties in the PINControl are:
public class PINControl : StackLayout
{
private const int LENGTH_DEFAULT = 4;
public static readonly BindableProperty PINLengthProp = BindableProperty.Create<PINControl, int> (c => c.PINLength, LENGTH_DEFAULT);
public static readonly BindableProperty PINCompleteCommandProp = BindableProperty.Create<PINControl, ICommand> (c => c.PINCompleteCommand, null);
public ICommand PINCompleteCommand {
get { return (ICommand)GetValue (PINCompleteCommandProp); }
set { SetValue (PINCompleteCommandProp, value); }
}
public int PINLength {
get { return (int)GetValue (PINLengthProp); }
set { SetValue (PINLengthProp, value); }
}
My ViewModel contains
public ICommand CompletePIN { get; set; }
public PINViewModel ()
{
CompletePIN = new Command<string> ((pin) => {
var e = pin.ToString();
});
}
It does not seem to have a problem with the PINLength, but the PINCompleteCommand is giving me the following error:
Cannot assign property "PINCompleteCommand": type mismatch between "Xamarin.Forms.Binding" and "System.Windows.Input.ICommand"
I cannot find a solution for this problem. Can somebody help me?
There is a good practice to follow while naming BindableProperties, which is to name it propertynameProperty.
In your case, when the Xaml parser encounter this instruction
PINCompleteCommand="{Binding CompletePIN}"
it first tries to find a public static BindableProperty with name PINCompleteCommandProperty, fails, then look for a normal property named PINCompleteCommand, succeed, and try to assign the value (a Binding) to the property (an ICommand) and generate the message you are seeing.
Fix your BindableProperty naming, and you should be fine.
Not sure it's applicable to the OP situation or not, but it's worth noting that this will also arise if the ViewModel/BindingTo property is the same name as the BindableProperty name and you bind the two together.
eg.
CustomControl
-> PinLengthCommand/PinLengthCommandProperty
ViewModel
-> PinLengthCommand
Just change the name of the property on the viewmodel and it will run OK.

How do I find the output model type in a behavior?

With FubuMVC, I'm not sure what the best way is to determine the current action's output model type. I see different objects that I could get the current request's URL from. But that doesn't lead to a very good solution.
What's the easiest way to get the current action's output model type from the behavior?
If this isn't a good practice, what's a better way?
First, I'm assuming you've already got your settings object(s) set up in StructureMap and have the ISettingsProvider stuff already wired up.
The best, simplest thing to do would be just to pull the settings in the view, like this:
<%: Get<YourSettingsObject>().SomeSettingProperty %>
If you insist on having these be a property on your output model, then continue reading:
Let's say you had a settings object like this:
public class OutputModelSettings
{
public string FavoriteAnimalName { get; set; }
public string BestSimpsonsCharacter { get; set; }
}
Then you had an output model like this:
public class OutputModelWithSettings
{
public string SomeOtherProperty { get; set; }
public OutputModelSettings Settings { get; set; }
}
You'll need to do a few things:
Wire up StructureMap so that it will do setter injection for Settings objects (so it will automatically inject the OutputModelSettings into your output model's "Settings" property.
Set up a setter injection policy in your StructureMap initialization code (a Registry, Global ASAX, your Bootstrapper, etc -- wherever you set up your container).
x.SetAllProperties(s => s.Matching(p => p.Name.EndsWith("Settings")));
Create your behavior to call StructureMap's "BuildUp()" on the output model to trigger the setter injection. The behavior will be an open type (i.e. on the end) so that it can support any kind of output model
public class OutputModelSettingBehavior<TOutputModel> : BasicBehavior
where TOutputModel : class
{
private readonly IFubuRequest _request;
private readonly IContainer _container;
public OutputModelSettingBehavior(IFubuRequest request, IContainer container)
: base(PartialBehavior.Executes)
{
_request = request;
_container = container;
}
protected override DoNext performInvoke()
{
BindSettingsProperties();
return DoNext.Continue;
}
public void BindSettingsProperties()
{
var viewModel = _request.Find<TOutputModel>().First();
_container.BuildUp(viewModel);
}
}
Create a convention to wire up the behavior
public class OutputModelSettingBehaviorConfiguration : IConfigurationAction
{
public void Configure(BehaviorGraph graph)
{
graph.Actions()
.Where(x => x.HasOutput &&
x.OutputType().GetProperties()
.Any(p => p.Name.EndsWith("Settings")))
.Each(x => x.AddAfter(new Wrapper(
typeof (OutputModelSettingBehavior<>)
.MakeGenericType(x.OutputType()))));
}
}
Wire the convention into your FubuRegistry after the Routes section:
ApplyConvention<OutputModelSettingBehaviorConfiguration>();
In your view, use the new settings object:
<%: Model.Settings.BestSimpsonsCharacter %>
NOTE: I have committed this as a working sample in the FubuMVC.HelloWorld project in the Fubu source. See this commit: https://github.com/DarthFubuMVC/fubumvc/commit/2e7ea30391eac0053300ec0f6f63136503b16cca