MAUI CreatePlatformView is never called? - c#

I have been trying out MAUI extensively lately and was trying to Create a Custom Control I guess and I ran into this weird problem, the CreatePlatform method was never getting called, at first I thought this was because I was using a MAUI class library and there was some issue with them, So instead I created another control in the same MAUI project instead of doing it through a CL and to my surprise even then it did not work.
My code is as follows:
Interface:
public interface IExtendedLabel : ILabel
{
bool HasUnderline { get; }
Color UnderlineColor { get; }
}
Label class:
public class ExtendedLabel : Label, IExtendedLabel
{
public readonly BindableProperty HasUnderlineProperty = BindableProperty.Create(
nameof(HasUnderline),
typeof(bool),
typeof(ExtendedLabel),
true);
public bool HasUnderline
{
get => (bool)GetValue(HasUnderlineProperty);
set => SetValue(HasUnderlineProperty, value);
}
public readonly BindableProperty UnderlineColorProperty = BindableProperty.Create(
nameof(UnderlineColor),
typeof(Color),
typeof(ExtendedLabel),
Colors.Black);
public Color UnderlineColor
{
get => (Color)GetValue(HasUnderlineProperty);
set => SetValue(HasUnderlineProperty, value);
}
}
My shared handler:
using System;
using MAUI.FreakyControls;
using Microsoft.Maui.Handlers;
#if ANDROID
using NativeView = AndroidX.AppCompat.Widget.AppCompatTextView;
#endif
#if IOS
using NativeView = UIKit.UILabel;
#endif
namespace Samples
{
public partial class ExtendedLabelHandler : ViewHandler<IExtendedLabel,NativeView>
{
#region ctor
public static CommandMapper<IExtendedLabel, ExtendedLabelHandler> CommandMapper = new(ViewCommandMapper);
public ExtendedLabelHandler() : base(FreakyEditorMapper)
{
}
public ExtendedLabelHandler(IPropertyMapper mapper = null) : base(mapper ?? FreakyEditorMapper)
{
}
#endregion
#region Mappers
public static IPropertyMapper<IExtendedLabel, ExtendedLabelHandler> FreakyEditorMapper = new PropertyMapper<IExtendedLabel, ExtendedLabelHandler>(ViewMapper)
{
[nameof(IExtendedLabel.HasUnderline)] = MapHasUnderlineWithColor,
[nameof(IExtendedLabel.UnderlineColor)] = MapHasUnderlineWithColor
};
public static void MapHasUnderlineWithColor(ExtendedLabelHandler handler, IExtendedLabel entry)
{
}
#endregion
}
}
Handler Android:
public partial class ExtendedLabelHandler
{
protected override AppCompatTextView CreatePlatformView()
{
var nativeView = new AppCompatTextView(this.Context)
{
};
return nativeView;
}
private void HandleNativeHasUnderline(bool hasUnderline, Color underlineColor)
{
if (hasUnderline)
{
var AndroidColor = underlineColor.ToNativeColor();
var colorFilter = BlendModeColorFilterCompat.CreateBlendModeColorFilterCompat(
AndroidColor, BlendModeCompat.SrcIn);
PlatformView.Background?.SetColorFilter(colorFilter);
}
else
{
PlatformView.Background?.ClearColorFilter();
}
}
}
My iOS handler:
public partial class ExtendedLabelHandler
{
CoreAnimation.CALayer bottomLine;
protected override UILabel CreatePlatformView()
{
return new UILabel();
}
private void HandleNativeHasUnderline(bool hasUnderline, Color underlineColor)
{
if (hasUnderline)
{
var uiColor = underlineColor.ToNativeColor();
bottomLine = BottomLineDrawer(uiColor);
bottomLine.Frame = new CGRect(x: 0, y: PlatformView.Frame.Size.Height - 5,
width: PlatformView.Frame.Size.Width, height: 1);
PlatformView.Layer.AddSublayer(bottomLine);
PlatformView.Layer.MasksToBounds = true;
}
else
{
bottomLine?.RemoveFromSuperLayer();
}
}
}
Adding the handler:
handlers.AddHandler(typeof(IExtendedLabel), typeof(ExtendedLabelHandler));
Am I doing something wrong?
You can find the full code on my repo here which has a full working example of the method never getting called for some reason: https://github.com/FreakyAli/MAUI.FreakyControls/tree/r1-gh/feat/freakyeditor
Update
So I am not sure if this is a bug or not but I have raised one in GitHub anyway, which can be tracked here: https://github.com/dotnet/maui/issues/9720

So the issue was my registration, I was registering my interface instead of the class of my custom control:
handlers.AddHandler(typeof(ExtendedLabel), typeof(ExtendedLabelHandler));

Related

Xamarin.iOS : round corner effect not working after updating Xamarin Form version to 5.0

i recently updated Xamarin form version to 5.0. After updating all the round effect corners are not working inside the App . We are currently using RoundCornersEffect. Below is the code reference.
[assembly: ExportEffect(typeof(RoundCornersEffect), "RoundCornersEffect")]
namespace Org.Code.iOS.Effects
{
public class RoundCornersEffect : PlatformEffect
{
protected override void OnAttached()
{
try
{
PrepareContainer();
SetCornerRadius();
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
protected override void OnDetached()
{
try
{
Container.Layer.CornerRadius = new nfloat(0);
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
{
if (args.PropertyName == RoundCornersSharedEffect.CornerRadiusProperty.PropertyName)
SetCornerRadius();
}
private void PrepareContainer()
{
Container.ClipsToBounds = true;
Container.Layer.AllowsEdgeAntialiasing = true;
Container.Layer.EdgeAntialiasingMask = CAEdgeAntialiasingMask.All;
}
private void SetCornerRadius()
{
var cornerRadius = RoundCornersSharedEffect.GetCornerRadius(Element);
Container.Layer.CornerRadius = new nfloat(cornerRadius);
}
}
}
And we have RoundCornersSharedEffect like this.
namespace Org.Code.Effects
{
public class RoundCornersSharedEffect : RoutingEffect
{
public RoundCornersSharedEffect() : base("App.RoundCornersEffect")
{
}
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.CreateAttached(
"CornerRadius",
typeof(int),
typeof(RoundCornersSharedEffect),
0,
propertyChanged: OnCornerRadiusChanged);
public static int GetCornerRadius(BindableObject view) =>
(int)view.GetValue(CornerRadiusProperty);
public static void SetCornerRadius(BindableObject view, int value) =>
view.SetValue(CornerRadiusProperty, value);
private static void OnCornerRadiusChanged(BindableObject bindable, object oldValue, object newValue)
{
if (!(bindable is View view))
return;
var cornerRadius = (int)newValue;
var effect = view.Effects.OfType<RoundCornersSharedEffect>().FirstOrDefault();
if (cornerRadius > 0 && effect == null)
view.Effects.Add(new RoundCornersSharedEffect());
if (cornerRadius == 0 && effect != null)
view.Effects.Remove(effect);
}
}
}
Please suggest how to resolve this round corner effect issue.
Use it above the namespace of your iOS->RoundCornersEffect class
[assembly: ResolutionGroupName("App")]
[assembly: ExportEffect(typeof(xxx.iOS.RoundCornersEffect), nameof(xxx.iOS.RoundCornersEffect))]
xxx.iOS.RoundCornersEffect Is the class you created in iOS(Change xxx to your project name).
For more information, please refer to the official document.

Create property dynamically in viewmodel MVVM light and UWP

I would like to create viewmodel properties in runtime.
I'm not so familiar with MVVM in UWP. Rather windows forms. In the past I created custom class object with reflection and I had possibility to add properties in runtime.
In current project I prepared solution with mvvm ligt and UWP app. Works fine with data exchange on viewmodel level. Now I try to find how to create properties of viewmodel in runtime ie. from descriptions in xml file.
namespace hmi_panel.ViewModels
{
public class HomeViewModel : ViewModelBase
{
#region Fields
readonly IPlcService _plcService;
#endregion
#region Constructors
public HomeViewModel(IPlcService dummyPlcService)
{
_plcService = dummyPlcService;
_plcService.Connect("127.0.0.1", 0, 1);
//zdarzenie cyklicznego odswiezania zeminnych
OnPlcServiceValuesRefreshed(null, null);
_plcService.ValuesRefreshed += OnPlcServiceValuesRefreshed;
}
#endregion
#region Properties
public string AppVersion
{
get { return $"{Package.Current.Id.Version.Major}.
{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}.
{Package.Current.Id.Version.Revision}"; }
}
public string AppCopyright
{
get { return "plc service: " + _plcService.ConnectionState.ToString(); }
}
private bool _pumpState;
public bool pumpState
{
get { return _pumpState; }
set {
_pumpState=value;
RaisePropertyChanged(() => pumpState);
}
}
#endregion
#region Methods
private RelayCommand _ConnectCommand;
public RelayCommand ConnectCommand
{
get
{
return _ConnectCommand ?? (_ConnectCommand = new RelayCommand(() =>
{
pumpState = true;
}, () => true));
}
}
private void OnPlcServiceValuesRefreshed(object sender, EventArgs e)
{
pumpState = _plcService.PumpState;
}
#endregion
}
}
Property pumpState value is readed and writed with _plService. I can change value and I can read after external change.
I would like to start only with bidirectional binding in xaml and create needed property ie. pumpState when viewmodel instance is created ie. in construtor.

Child properties update calling it's parent's `OnPropertyChanged`

I'm trying to create a XF component whose some properties are of a type that inherits from BindableObject. For illustrating, I have class Shadow with double Radius and Color ShadowColor properties and a class MyBoxText, that have a bool IsLoading and a Shadow Ghost properties.
My View and it's Bindings is working as expected, but I have an issue with it's custom renderer:
When I change the Ghost properties I need to redraw the entire view (of the MyBoxText control), to visually update the shadow color, for example.
Here's some mcve:
Classes code:
public class MyBoxText : Label /* It's bindable by inheritance */
{
#region Properties
public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(MyBoxText), false) ;
public bool IsLoading
{
get { return (bool)GetValue(IsLoadingProperty); }
set { SetValue(IsLoadingProperty, value); }
}
public static readonly BindableProperty GhostProperty = BindableProperty.Create(nameof(Ghost), typeof(Shadow), typeof(MyBoxText), null) ;
public Shadow Ghost
{
get { return (Shadow)GetValue(GhostProperty); }
set { SetValue(GhostProperty, value); }
}
#endregion
}
public class Shadow : BindableObject /* It's explictly bindable */
{
#region Properties
public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(Shadow), Color.Black) ;
public Color ShadowColor
{
get { return (Color)GetValue(ShadowColorProperty); }
set { SetValue(ShadowColorProperty, value); }
}
public static readonly BindableProperty ShadowRadiusProperty = BindableProperty.Create(nameof(ShadowRadius), typeof(double), typeof(Shadow), 20) ;
public double ShadowRadius
{
get { return (double)GetValue(ShadowRadiusProperty); }
set { SetValue(ShadowRadiusProperty, value); }
}
#endregion
public Shadow()
{
}
}
My renderer's code is like this:
public class MyBoxText : LabelRenderer
{
public MyBoxText()
{
SetWillNotDraw(false);
}
public override void Draw(Canvas canvas)
{
MyBoxText myView = (MyBoxText)this.Element;
// Some drawing logic
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == MyBoxText.IsLoadingProperty.PropertyName ||
e.PropertyName == MyBoxText.GhostProperty.PropertyName )
Invalidate();
}
}
The issue is that when I change the Ghost.ShadowColor property my 'OnElementPropertyChanged' override is not called, and the View stays with the old color on the screen.
Is there a way to propagate the child's 'Property Update' event to parent view 'Property Changed' or another way to achieve this?
The issue is that when I change the Ghost.ShadowColor property my 'OnElementPropertyChanged' override is not called, and the View stays with the old color on the screen.
Is there a way to propagate the child's 'Property Update' event to parent view 'Property Changed' or another way to achieve this?
Yes, there is a way. Since your Shadow inherits from BindableObject, which implements the INotifyPropertyChanged Interface. You can set notify ShadowColor change:
Add OnPropertyChanged() to Setter of ShadowColor in Shadow.cs:
public class Shadow : BindableObject /* It's explictly bindable */
{
#region Properties
public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(Shadow), Color.Black);
public Color ShadowColor
{
get { return (Color)GetValue(ShadowColorProperty); }
set { SetValue(ShadowColorProperty, value);
//Notify the ShadowColorProperty Changed
OnPropertyChanged();
}
}
...
}
Modify your MyBoxText.cs like this:
public class MyBoxText : Label /* It's bindable by inheritance */
{
...
public static readonly BindableProperty GhostProperty = BindableProperty.Create(nameof(Ghost), typeof(Shadow), typeof(MyBoxText), null);
public Shadow Ghost
{
get { return (Shadow)GetValue(GhostProperty); }
set {
//register the ShadowColor change event
value.PropertyChanged += ShadowColor_PropertyChanged;
SetValue(GhostProperty, value); }
}
private void ShadowColor_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
//unregister the event
this.Ghost.PropertyChanged -= ShadowColor_PropertyChanged;
//set this.Ghost to a new object with new ShadowColor to trigger the OnPropertyChanged
this.Ghost = new Shadow
{
ShadowColor = (sender as Shadow).ShadowColor,
ShadowRadius = Ghost.ShadowRadius
};
}
}
Thanks to Elvis's answer I got it. Based on his idea I've made some changing to reuse it on other components and I'm sharing it now just in case someone else needs something like this.
I thought that use it this way we could get a cleaner and simple code:
public class MyBoxText : Label /* It's bindable by inheritance */
{
// Added this as private property
private ChangingPropagator changingPropagator;
private ChangingPropagator ChangingPropagator
{
get
{
if (changingPropagator == null)
changingPropagator = new ChangingPropagator(this, OnPropertyChanged, nameof(Shadow.ShadowColor), nameof(Shadow.ShadowRadius));
return changingPropagator;
}
}
#region Properties
public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(MyBoxText), false) ;
public bool IsLoading
{
get { return (bool)GetValue(IsLoadingProperty); }
set { SetValue(IsLoadingProperty, value); }
}
public static readonly BindableProperty GhostProperty = BindableProperty.Create(nameof(Ghost), typeof(Shadow), typeof(MyBoxText), null) ;
public Shadow Ghost
{
// Here I use the ChangingPropagator's Getter and Setter instead of the deafult ones:
get { return ChangingPropagator.GetValue<Shadow>(GhostProperty); }
set { ChangingPropagator.SetValue(GhostProperty,ref value); }
}
#endregion
}
And it's the ChangingPropagator class:
public class ChangingPropagator
{
string[] listenedProperties = new string[0];
Action<string> changesNotifyer = null;
BindableObject propagationRootObject = null;
List<KeyValuePair<string, object>> propagationProperties = new List<KeyValuePair<string, object>>();
public ChangingPropagator(BindableObject bindableObject, Action<string> onPropertyChangedMethod, params string[] propertyToListenTo)
{
changesNotifyer = onPropertyChangedMethod;
propagationRootObject = bindableObject;
listenedProperties = propertyToListenTo ?? listenedProperties;
// ToDo: Add some consistency checks
}
public void AddPropertyToListenTo(params string[] propertyName)
{
listenedProperties = listenedProperties.Union(propertyName).ToArray();
}
// I need handle it here too 'cause when I use the child `Ghost` property coming from XAML binding, it didn't hit the `set` method
public T GetValue<T>(BindableProperty property)
{
var value = propagationRootObject?.GetValue(property);
if (value != null)
{
INotifyPropertyChanged bindableSubObject = (value as INotifyPropertyChanged);
if (bindableSubObject != null)
{
bindableSubObject.PropertyChanged -= PropagatorListener;
bindableSubObject.PropertyChanged += PropagatorListener;
if (!propagationProperties.Any(a => a.Key == property.PropertyName))
propagationProperties.Add(new KeyValuePair<string, object>(property.PropertyName, value));
}
}
return (T)value;
}
public void SetValue<T>(BindableProperty property, ref T value)
{
var oldValue = propagationRootObject?.GetValue(property);
if (oldValue != null)
{
INotifyPropertyChanged bindableSubObject = (value as INotifyPropertyChanged);
if (bindableSubObject != null)
bindableSubObject.PropertyChanged -= PropagatorListener;
}
if (value != null)
{
INotifyPropertyChanged bindableSubObject = (value as INotifyPropertyChanged);
if (bindableSubObject != null)
{
bindableSubObject.PropertyChanged += PropagatorListener;
propagationProperties.RemoveAll(p => p.Key == property.PropertyName);
propagationProperties.Add(new KeyValuePair<string, object>(property.PropertyName, value));
}
}
propagationRootObject.SetValue(property, value);
}
private void PropagatorListener(object sender, PropertyChangedEventArgs e)
{
if (listenedProperties?.Contains(e.PropertyName) ?? true)
PropagationThrower(sender);
}
private void PropagationThrower(object sender)
{
if (propagationProperties.Any(p => p.Value == sender))
{
var prop = propagationProperties.FirstOrDefault(p => p.Value == sender);
changesNotifyer?.Invoke(prop.Key);
}
}
}

How to access method from view inside a Xamarin Forms custom renderer?

I have the following code:
public partial class PhrasesFrameRendererClass : Frame
{
.....
void getRandomWords() {
// more code here that involves getting random numbers
// and updating a grid's bindingcontext
}
}
In my custom renderer I want to be able to call the getRandomWords on swipe left gesture like below:
public class PhraseFrameCustomRenderer : FrameRenderer
{
UISwipeGestureRecognizer leftSwipeGestureRecognizer;
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
leftSwipeGestureRecognizer = new UISwipeGestureRecognizer();
leftSwipeGestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Left;
leftSwipeGestureRecognizer.NumberOfTouchesRequired = 1;
leftSwipeGestureRecognizer.AddTarget((obj) =>
{
// Call getRandomWords() here
});
}
}
Is this possible? Any ideas on how this could be done?
base.OnElementChanged(e);
leftSwipeGestureRecognizer = new UISwipeGestureRecognizer();
leftSwipeGestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Left;
leftSwipeGestureRecognizer.NumberOfTouchesRequired = 1;
leftSwipeGestureRecognizer.AddTarget((obj) =>
{
// Call getRandomWords() here
var frame = Element as PhrasesFrameRendererClass ;
if(frame!=null){
frame.getRandomWords();
}
});
You can create a BindableProperty of type Command in your custom frame class, call that Command from your renderer and bind your ViewModel's getRandomWords method as a Command
//Your custom control in your PCL project
public partial class PhrasesFrameRendererClass : Frame
{
public static readonly BindableProperty SwipeLeftCommandProperty =
BindableProperty.Create(nameof(SwipeLeftCommand), typeof(ICommand), typeof(PhrasesFrameRendererClass ), null);
public ICommand SwipeLeftCommand
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
}
//Your custom control renderer
public class PhraseFrameCustomRenderer : FrameRenderer
{
UISwipeGestureRecognizer leftSwipeGestureRecognizer;
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
leftSwipeGestureRecognizer = new UISwipeGestureRecognizer();
leftSwipeGestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Left;
leftSwipeGestureRecognizer.NumberOfTouchesRequired = 1;
leftSwipeGestureRecognizer.AddTarget((obj) =>
{
var myFrame = Element as PhrasesFrameRendererClassl
if(myFrame != null){
if(myFrame.SwipeLeftCommand != null && myFrame.SwipeLeftCommand.CanExecute()){
myFrame.SwipeLeftCommand.Execute();
}
}
});
}
}
//Your ViewModel
public class PhrasesViewModel{
public Command GetRandomWordsCommand {get;set;}
public PhrasesViewModel(){
GetRandomWordsCommand = new Command(ExecuteGetRandomWords);
}
private void ExecuteGetRandomWords(){
//Your method goes here
}
}
//Your XAML
<yourControls:PhrasesFrameRendererClass SwipeLeftCommand="{Binding GetRandomWordsCommand }"/>
It may seem more complicated this way, but using commands allows you to separate your application code (Such as getting random phrases) from your rendering code

Why custom control task pane does not update its properties?

I have designed a custom panel which can expand or collapse form at run time.
When I change its height from custom designed task, it does not update it.
Code of my control class:
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Windows.Forms.Design;
[Designer(typeof(MyControlDesigner))]
public partial class ExpandCollapsePanel : UserControl
{
private bool flag = false;
private Size size;
public int usrVerticalSize;
public ExpandCollapsePanel()
{
InitializeComponent();
}
[DefaultValueAttribute(true)]
public int SetVerticalSize
{
get
{
return usrVerticalSize;
}
set
{
usrVerticalSize = value;
}
}
Code of taskpanedesign class:
namespace ExpandCollapseFormLibrary
{
class CustomDialogue : ControlDesigner
{
private DesignerActionListCollection actionLists;
public override DesignerActionListCollection ActionLists
{
get
{
if (actionLists == null)
{
actionLists = new DesignerActionListCollection();
actionLists.Add(new MyActionListItem(this));
}
return actionLists;
}
}
}
internal class MyActionListItem : DesignerActionList
{
public MyActionListItem(ControlDesigner owner) : base(owner.Component)
{
}
public override DesignerActionItemCollection GetSortedActionItems()
{
var items = new DesignerActionItemCollection();
//items.Add(new DesignerActionTextItem("Hello world", "Misc"));
items.Add(new DesignerActionPropertyItem("Checked", "Vertical Drop Down Size"));
return items;
}
public int Checked
{
get { return ((ExpandCollapsePanel)base.Component).SetVerticalSize; }
set { ((ExpandCollapsePanel)base.Component).SetVerticalSize = value; }
}
}
}
When I change the value the Form1(where drag and dropped) designed class keep it permanently.
the SetVerticalSize property value of your custom pane's is really changed, but the problem is that the designer host does not know about it at all. To notify the designer host about your custom pane changing you should implement something like this (I suggest you read the IComponentChangeService MSDN article for more details):
int usrVerticalSize;
[DefaultValue(true)]
public int SetVerticalSize {
get { return usrVerticalSize; }
set {
FireChanging(); //changing notification
try {
usrVerticalSize = value;
}
finally { FireChanged(); } //changed notification
}
}
void FireChanging() {
IComponentChangeService service = GetComponentChangeService();
if(service != null)
service.OnComponentChanging(this, null);
}
void FireChanged() {
IComponentChangeService service = GetComponentChangeService();
if(service != null)
service.OnComponentChanged(this, null, null, null);
}
IComponentChangeService GetComponentChangeService() {
return GetService(typeof(IComponentChangeService)) as IComponentChangeService;
}

Categories

Resources