"Control" Property In Custom Renderers and Effects Is Null For Layouts - xamarin.forms

When I try to write a custom renderer for any of the layout classes, the "Control" property appears null. It is OK since the renderer already is the wrapper for the native control but it is a problem when creating an Effect. So I cannot do any customization with the layout classes using Effects since Control property is not assigned. Is this a bug?

The PlatformEffect class has another property called Container which could also be used for the effect when the Control is null, e.g. at layouts.

So I saw the same issue as the original post.
I was inheriting PlatformEffect<UIView, UILabel> on iOS. The control was null when I was debugging. I changed my Effect to inherit from PlatformEffect and then cast the control as ((UILabel)Control) and everything started working.
Returned a Null Control
MultilineTruncateLabelEffect : PlatformEffect<UIView, UILabel>
{
protected override void OnAttached()
{
var effect = (MultilineEffect) Element.Effects.FirstOrDefault(e => e is MultilineEffect);
if (effect != null)
{
Control.Lines = effect.Lines;
}
}
}
Worked Fine
public class MultilineTruncateLabelEffect : PlatformEffect
{
protected override void OnAttached()
{
var effect = (MultilineEffect) Element.Effects.FirstOrDefault(e => e is MultilineEffect);
if (effect != null)
{
((UILabel)Control).Lines = effect.Lines;
}
}
}

Layout do not have renderers, and as they have no renderers or native views (i.e. no Control), there's nothing you could possibly modify with an Effect.

Related

Interaction with parent control triggers RippleDrawable in Xamarin.Forns custom renderer

I have implemented a custom clickable label class in Xamarin.Forms along with a custom renderer, that adds a RippleDrawable as the controls Foreground. I am creating the RippleDrawable with the following code:
public static Drawable CreateRippleDrawable(Context context)
{
var typedValue = new TypedValue();
context.Theme.ResolveAttribute(Resource.Attribute.SelectableItemBackground, typedValue, true);
var rippleDrawable = context.Resources.GetDrawable(typedValue.ResourceId, context.Theme);
return rippleDrawable;
}
In my custom renderer I assign the drawable
this.Control.Foreground = DrawableHelper.CreateRippleDrawable(this.Context);
and update the ripple when the user touches the control
private void LinkLabelRenderer_Touch(object sender, TouchEventArgs e)
{
if (e.Event.Action == MotionEventActions.Down)
{
this.Pressed = true;
}
if (e.Event.Action == MotionEventActions.Cancel)
{
this.Pressed = false;
}
if (e.Event.Action == MotionEventActions.Up)
{
this.Ripple.SetHotspot(e.Event.GetX(), e.Event.GetY());
this.Pressed = false;
// raise the event of the Xamarin.Forms control
}
}
Now, whenever I click the control, the ripple will be shown, which is the expected behavior, but if I touch (tap or long-press) the parents of the control (e.g. the StackLayout, Grid or whatever layout contains the label, including their parent Layout, Page or View) the ripple animation will be triggered. Anyway, the event handler LinkLabelRenderer_Touch in not called in this case, only when the actual control is touched.
I can work around this behavior by adding an empty GestureRecognizer to the respective parent(s), but I really dislike this solution, because this is but a hack. And to make things worse it is a hack I'll always have to remember whenever I use the control.
How can I prevent the RippleDrawable being shown when the parent is touched?
Turned out I got things fundamentally wrong. Subscribing the Touch event is not the way to go. I had to make the control clickable and subscribe the Click event
this.Control.Clickable = true;
this.Click += LinkLabelRenderer_OnClick;
There is no need to handle all that RippleTouch stuff the way I did (via the Touch event) but could let android handle things for me.

Xamarin Forms iOS renderer Dimensions

I’m trying to create a “generic” renderer for iOS and I have trouble figuring out the dimensions of the renderer.
The class of the renderer in the PCL is very simple and the only “peculiar” is that inherits from a View since I want to have it as a generic renderer
public class ExampleView : View { }
In the XAML, it is also very typical
<renderers:ExampleView />
The definition of the renderer in the iOS project is the following
public class ExampleRenderer : ViewRenderer<ExampleView, UIView>
{
protected override void OnElementChanged(ElementChangedEventArgs<ExampleView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
SetNativeControl(new UIView(new CGRect(0, 0, 0, 300)));
}
}
}
The interesting part here is that the class inherits from ViewRenderer<ExampleView, UIView>
It seems that although I specify that the width of the render to be 0, the actual renderer takes the entire width of the page.
In fact whatever value I put as width, will be disregarded, and the renderer will occupy the entire page.
This is not bad, it is unintentional and this is what bother me. (or Am I doing something wrong?)
So the question here is how can find in my custom render, the width that occupies?
It is not the renderer, but the native control that is taking up the space. The renderer is doing it's job i.e. keep the native control in sync with forms control - which includes layout parse requests.
To control the size constraints I would recommend using the properties WidthRequest or HeightRequest on forms Element to do that. It will automatically trickle down to the native Control.Frame through the renderer.
EDIT-1
If you want to update your native controls (or subviews) based on current bounds you can use OnElementPropertyChanged to track the changes. There can many reasons that can cause a size-update - like device-orientation change, parent layout, first layout pass etc.
public class ExampleRenderer : ViewRenderer<ExampleView, UIView>
{
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == VisualElement.HeightProperty.PropertyName)
{
if(Element != null && Element.Bounds.Height > 0 && Element.Bounds.Width > 0)
{
//update size on native control's subviews
// or use SetNeedsDisplay() to re-draw
//Note: force update to Control's size is not recommended (as it is recommended it be same as forms element)
//Instead add a subview that you can update for size here
}
}
}

Rewiring actions of parent to a child viewmodel

So here's my screnario. I have a toolbar at the top (office style), with buttons. This is hosted in a shell. Some of those buttons are applicable only to certain child view models as they get loaded. Ideally what I would like to happen is have the buttons action.target repositioned to child view model as it gets created (I kind of got this working by settings Action.Target="ActiveItem" on them. This doesn't solve the problem fully though:
a) When the child viewmodel is closed and there is no active item, I want them to reposition to Shell as the target so they can be set to "default" state.
b) I noticed that when child viewmodel is closed and the shell being the conductor has it ActiveItem=null, the hooks from the action are still bound to the living instance of the last viewmodel, so doesn't looks like it got disposed of. Memory leak?
Any suggestions how to implement this scenario?
What about adding a property to your ShellViewModel which points to the action target and updating it when stuff gets activated/deactivated:
e.g.
public class ShellViewModel
{
public object ActionTarget
{
get { return _actionTarget; }
set
{
_actionTarget = value;
NotifyOfPropertyChange(() => ActionTarget);
}
}
// Then when the active item changes just update the target:
public override NotifyOfPropertyChange(string propertyName)
{
if(propertyName == "ActiveItem")
{
if(ActiveItem == null) ActionTarget = this;
else ActionTarget = ActiveItem;
}
}
}
Now bind to that:
<SomeMenu cal:Action.Target="{Binding ActionTarget}" />
Not sure if that will work or not but I'm sure I've done something similar in the past. (You may also have to explicitly call NPC on your actions before they will update after you have changed ActiveItem)

Set Flex AxisRenderer's axisStroke via CSS

I'm trying to set the axisStroke style on AxisRenderer via CSS. It's of type IStroke. How do I create a Stroke in CSS?
Someone :) suggested I should post a link to my InsideRIA about how to "tweak" a CSSStyleDeclaration to add the stroke information so that you don't have to override AxisRenderer (or have an explicit AxisRenderer at all) http://insideria.com/2010/08/usinc-css-for-strokes-on-flex.html,
Additionally, I'd like to add that I've started dealing with custom styles in a way that's a little different than how the examples show. It looks something like this:
protected var _myStyleValue:SomeType = someValueThatWouldNeverOccurInNature;
protected var _myThingTheStyleIsAttachedTo:IStyleClient;
//set this flag whenever you want to refresh the object
protected var _myThingTheStyleIsAttachedToChanged:Boolean;
override protected function commitProperties():void {
if (_myThingTheStyleIsAttachedToChanged) {
var myStyleValue:SomeType = getMyStyleValue();
_myThingTheStyleIsAttachedTo.setStyle('myStyle', myStyleValue);
_myThingTheStyleIsAttachedToChanged = false;
}
}
override public function styleChanged(styleProp:String):void {
super.styleChanged(styleProp);
if (styleProp=='myStyle') {
_myThingTheStyleIsAttachedToChanged = true;
_myStyleValue = someValueThatWouldNeverOccurInNature;
}
}
protected function getMyStyleValue():SomeType {
if (_myStyleValue==someValueThatWouldNeverOccurInNature) {
var theValue:*=getStyle('myStyle');
if (theValue==undefined) {
_myStyleValue=theDefaultValue;
} else {
_myStyleValue=theValue as SomeType;
}
return _myStyleValue;
}
}
It's a little more verbose than what you usually see, especially if there are multiple properties you need to look at. However, it sidesteps issues about styles applied in MXML vs. CSS style declaration, as well as what happens when a style changes. It's also "lazy", and so may not get called if it is not needed.
Why not try my (admittedly, somewhat crazy) CSS post-processing trick: http://bitrotincarnate.wordpress.com/2009/12/16/strokes-and-fills-in-flex-css-through-actionscript-injection/
#James:
Looks like a little bug here:
if ((getStyle("axisStrokeColor") != null) && (getStyle("axisStrokeColor") != null))
{
var axisStroke:Stroke = new Stroke(getStyle("axisStrokeColor"), getStyle("axisStrokeColor"));
Repeating the axisStrokeColor twice. Think you meant to refer to strokeWeight as well:
if ((getStyle("axisStrokeWeight") != null) && (getStyle("axisStrokeColor") != null))
{
var axisStroke:Stroke = new Stroke(getStyle("axisStrokeColor"), getStyle("axisStrokeWeight"));
Not sure I understand the guts of your code logic above, although I do understand its purpose. ;-)
However, relying on styleChanged() alone did not work for me. Particularly if I switched CSS styles during run-time and and the new style declaration didn't have "axisStroke" in it -- which of course it can't! The fuller solution would be to include code for styleChanged() and also stylesInitialized(). In the source, I also include a reference to the stroke object _axisStroke. This cuts down countless calls to getStyle("axisStroke") as well as being necessary for the guaranteed instantiation logic below.
Like I said, my problem was I would get run-time errors if I declared a style entry in CSS for an axis renderer component but did not set its "axisStroke" style property programatically. Although the work-around for this is usually to instantiate a default CSSStyleDeclaration in a classConstructed() static method (see adobe's developer guide and styles for this common practice), this will not work if the style CSS is changed dynamically during run-time. The classConstructed() only gets called initially and usually checks to make sure there is no style declaration already for the component before registering the default. Then later, when you change styles, your component's "axisStroke" style gets cleared. THEN, you can get null exceptions. Furthermore, just using CSS, you can't insert a new "axisStroke" style. But, since stylesInitialized() is called anytime a component's style selector setting changes -- eg: StyleManager.setStyleDeclaration("MyComponent", myStyleDeclaration) -- you can insure all the styles are set and in place, and anything that might throw a null exception, such as a missing "axisStroke" style, can be accounted for.
private var _axisStroke:IStroke;
override public function stylesInitialized():void {
super.stylesInitialized();
_axisStroke = getStyle("axisStroke");
if (!_axisStroke) {
//usually a default stroke style is set in a default CSSStyleDeclaration
//created in a classConstructed() method
_axisStroke = new Stroke(0xCCDDEE, 8, 1, false, LineScaleMode.NORMAL, CapsStyle.NONE);
//note: I do NOT register _axisStroke to the style "axisStroke".
//Since "axisStroke" is still technically not set, I leave it null.
//You may need to set it, however, if you are extending AxisRenderer.
//setStyle("axisStroke", _axisStroke);
}
var axisColor:Number = getStyle("axisColor");
if (!isNaN(axisColor)) {
Object(_axisStroke).color = axisColor;
}
...
}
Anyway, if just using styleChanged() works for you, great. But if you still get run-time errors from styles not being initialized or in the wrong order, look into using stylesInitialized(). This is particularly useful when making your own IAxisRenderer classes.
The solution I finally used was to create a custom extension of AxisRenderer that exposed the styles I needed:
package
{
import mx.charts.AxisRenderer;
import mx.graphics.Stroke;
[Style(name="axisStrokeColor", type="uint", format="Color", inherit="yes")]
[Style(name="axisStrokeWeight", type="Number", format="Length", inherit="yes")]
[Style(name="minorTickStrokeColor", type="uint", format="Color", inherit="yes")]
[Style(name="minorTickStrokeWeight", type="Number", format="Length", inherit="yes")]
[Style(name="tickStrokeColor", type="uint", format="Color", inherit="yes")]
[Style(name="tickStrokeWeight", type="Number", format="Length", inherit="yes")]
public class StyledAxisRenderer extends AxisRenderer
{
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
var stylesUpdated:Boolean = false;
if ((styleProp != "axisStroke") && (styleProp != "minorTickStroke") && (styleProp != "tickStroke"))
{
if ((getStyle("axisStrokeColor") != null) && (getStyle("axisStrokeWeight") != null))
{
var axisStroke:Stroke = new Stroke(getStyle("axisStrokeColor"), getStyle("axisStrokeWeight"));
setStyle("axisStroke", axisStroke);
stylesUpdated = true;
}
if ((getStyle("minorTickStrokeColor") != null) && (getStyle("minorTickStrokeWeight") != null))
{
var minorTickStroke:Stroke = new Stroke(getStyle("minorTickStrokeColor"), getStyle("minorTickStrokeWeight"));
setStyle("minorTickStroke", minorTickStroke);
stylesUpdated = true;
}
if ((getStyle("tickStrokeColor") != null) && (getStyle("tickStrokeWeight") != null))
{
var tickStroke:Stroke = new Stroke(getStyle("tickStrokeColor"), getStyle("tickStrokeWeight"));
setStyle("tickStroke", tickStroke);
stylesUpdated = true;
}
if (stylesUpdated)
{
invalidateDisplayList();
}
}
}
}
}

Flex Advanced Datagrid Condition Row Background Color

I am trying to set the row background color for the advanced data grid control in Flex 3. Does anyone know if this is possible using a style function. Currently my style function looks like:
public function myStyleFunc(data:Object, col:AdvancedDataGridColumn):Object
{
if (data["status"] == "PRICING")
return {color:0xFF0000 , fontWeight:"bold" , backgroundColor:0xFF0000};
// Return null if the Artist name does not match.
return null;
}
However the background color does not change.
I have heard on the grape vine that I may need to override some methods to enable the background color property.
Any help would be appreciated .
Regards Karl
I have done some thing like that but in my case color was also coming from data also but it will help you.
You have to override the Datagrid and override drawRowBackground method
public class CustomDataGrid extends AdvancedDataGrid
{
protected override function drawRowBackground(s:Sprite, rowIndex:int, y:Number, height:Number, color:uint, dataIndex:int):void{
var XMLdata:XML=rowNumberToData(dataIndex) as XML;
if(XMLdata!=null){
if(XMLdata.attribute(Constants.col) != undefined && XMLdata.attribute(Constants.col) != ""){
color=XMLdata.attribute(Constants.col);
}else{
color=0xFFFFFF;
}
}
super.drawRowBackground(s,rowIndex,y,height,color,dataIndex);
}
}
By this you can get any data from the row and according to it give the color.

Resources