I have a custom renderer to display HTML formatted text in a UITextView. If I hard-code the text into the constructor of the page that contains the control (so it gets set in the custom control's OnElementChanged event), it displays fine. If I await a api call to get the text and then set it (so it gets set in the custom control's OnElementPropertyChanged event) it does not repaint. If I change the orientation of the device, the text appears. What do I need to add to get it to display the text when it is set?
[assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
namespace MyApp.iOS.Renderers
{
class HtmlLabelRenderer : ViewRenderer<HtmlLabel, UITextView>
{
private UITextView _htmlTextView = new UITextView();
protected override void OnElementChanged(ElementChangedEventArgs<HtmlLabel> e)
{
base.OnElementChanged(e);
if (Element?.Text == null) return;
SetHtmlText(Element.Text);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (string.Equals(e.PropertyName, "Text", StringComparison.CurrentCultureIgnoreCase))
{
SetHtmlText(((HtmlLabel)sender).Text);
_htmlTextView.SetNeedsDisplay();
}
base.OnElementPropertyChanged(sender, e);
}
private void SetHtmlText(string text)
{
var attr = new NSAttributedStringDocumentAttributes {DocumentType = NSDocumentType.HTML};
var nsError = new NSError();
_htmlTextView.Editable = false;
_htmlTextView.AttributedText = new NSAttributedString(text, attr, ref nsError);
_htmlTextView.DataDetectorTypes = UIDataDetectorType.All;
SetNativeControl(_htmlTextView);
}
}
}
Update : I got further by changing the OnElementChanged to:
protected override void OnElementChanged(ElementChangedEventArgs<HtmlLabel> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || Element == null) return;
SetHtmlText(e.NewElement.Text ?? string.Empty);
SetNativeControl(_htmlTextView);
}
now if I have more than one HtmlLabel on the page all except the first one displays.
Try changing from:
_htmlTextView.SetNeedsDisplay();
to
SetNeedsDisplay();
or,
(Control ?? NativeView).SetNeedsDisplay();
Sharada Gururaj is right, changing to derive from Editor worked for iOS .. but breaks Android. Though this seems brute force, I used conditional compilation to get it working...
namespace MyApp.Renderers
{
#if __IOS__
public class HtmlLabel : Editor
{
}
#else
public class HtmlLabel : Label
{
}
#endif
}
Here is the android
[assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
namespace MyApp.Droid.Renderers
{
public class HtmlLabelRenderer : LabelRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
var view = (HtmlLabel)Element;
if (view?.Text == null) return;
SetHtmlText(view.Text);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Label.TextProperty.PropertyName)
{
SetHtmlText(((HtmlLabel) sender).Text);
}
}
private void SetHtmlText(string text)
{
var encodedText = (((int)Build.VERSION.SdkInt) >= 24) ? Html.FromHtml(text, FromHtmlOptions.ModeLegacy) :
#pragma warning disable 618
// need this for backward compatability
Html.FromHtml(text);
#pragma warning restore 618
Control.MovementMethod = LinkMovementMethod.Instance;
Control.SetText(encodedText, TextView.BufferType.Spannable);
}
}
}
Your custom HtmlLabel class should be able to derive from the same thing on Android and iOS
namespace YourNameSpace
{
public class HtmlLabel : Label
{
}
}
The renderer for Android should look something like this
[assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
namespace YourNameSpace.Droid
{
public class HtmlLabelRenderer : ViewRenderer<Label, TextView>
{
TextView _textView;
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (Element == null)
return;
if(Control == null)
{
_textView = new TextView(Context);
SetHtmlText(Element.Text);
SetNativeControl(_textView);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Element == null || Control == null)
return;
if (e.PropertyName == HtmlLabel.TextProperty.PropertyName)
{
SetHtmlText(Element.Text);
}
}
private void SetHtmlText(string text)
{
if (Android.OS.Build.VERSION.SdkInt >= BuildVersionCodes.N)
{
_textView.TextFormatted = Html.FromHtml(text, Android.Text.FromHtmlOptions.ModeCompact);
}
else
{
_textView.TextFormatted = Html.FromHtml(text);
}
}
}
}
And on iOS it should look very similar
[assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
namespace YourNameSpace.iOS
{
public class HtmlLabelRenderer : ViewRenderer<Label, UITextView>
{
UITextView _textView;
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (Element == null)
return;
if(Control == null)
{
_textView = new UITextView();
SetHtmlText(Element.Text);
SetNativeControl(_textView);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Element == null || Control == null)
return;
if (e.PropertyName == HtmlLabel.TextProperty.PropertyName)
{
SetHtmlText(Element.Text);
}
}
private void SetHtmlText(string text)
{
var attr = new NSAttributedStringDocumentAttributes { DocumentType = NSDocumentType.HTML };
var nsError = new NSError();
_textView.Editable = false;
_textView.AttributedText = new NSAttributedString(text, attr, ref nsError);
_textView.DataDetectorTypes = UIDataDetectorType.All;
}
}
}
I tested that on Android and it worked, calling OnElementPropertyChanged when the text changed and everything. However, I don't have a mac at home to try the iOS Renderer so I'm just assuming it will function pretty much the same.
Related
I have a PopUpWindowShowAction that operates on the current record.
If there is no current record then I want the action disabled.
This is because if there is no record the PopUpWindowShowAction will fail.
Here is my simplified controller
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.ExpressApp.Editors;
using System;
using System.Linq;
using System.Windows.Forms;
namespace MyNamespace
{
public partial class JobWorkflowController : ViewController
{
PopupWindowShowAction actWorkflow;
public JobWorkflowController()
{
TargetObjectType = typeof(IWorkflow);
actWorkflow = new PopupWindowShowAction(this, "Workflow", "Admin")
{ AcceptButtonCaption = string.Empty, ActionMeaning = ActionMeaning.Accept, CancelButtonCaption = null, Caption = "Workflow", ConfirmationMessage = null, ImageName = "Workflow", Shortcut = "F7", ToolTip = null };
actWorkflow.CustomizePopupWindowParams += actWorkflow_CustomizePopupWindowParams_1;
actWorkflow.Execute += actWorkflow_Execute_1;
actWorkflow.Cancel += actWorkflow_Cancel;
}
private void actWorkflow_CustomizePopupWindowParams_1(object sender, CustomizePopupWindowParamsEventArgs e)
{
if (View.CurrentObject is not IWorkflow wf)
{
// causes an error because the view is not set
return;
}
// code to create the popup view
}
private void actWorkflow_Execute_1(object sender, PopupWindowShowActionExecuteEventArgs e)
{
// code
}
private void actWorkflow_Cancel(object sender, EventArgs e)
{
// code
}
protected override void OnActivated()
{
base.OnActivated();
View.CurrentObjectChanged += View_CurrentObjectChanged;
View_CurrentObjectChanged(View, new EventArgs());
}
private void View_CurrentObjectChanged(object sender, EventArgs e)
{
actWorkflow.Enabled["HasCurrent"]= View.CurrentObject != null;
}
protected override void OnDeactivated()
{
View.CurrentObjectChanged -= View_CurrentObjectChanged;
base.OnDeactivated();
}
}
}
The View_CurrentObjectChanged event fires but the action does not disable.
[Update]
I tried Michael's suggestion but the action des not disable.
Put this in your constructor
actWorkflow.SelectionDependencyType = SelectionDependencyType.RequireSingleObject
And it will only be active when a single object is selected. If you'd like to have one or more objects selected it's:
actWorkflow.SelectionDependencyType = SelectionDependencyType.RequireMultipleObjects;
You'll have no need to subscribe to the CurrentObjectChanged event.
I have a Xamarin mobile app who include a WebView.
This one display an Angular Website.
When I change my mobile orientation, our Angular code doesn't catch this event on iOS (WKWebView).
But I don't have any troubles with the Android WebView, Resize and screen orientation events are catched.
Then I supposed my problem is on the iOS Xamarin part.
Angular window resize event catch :
#HostListener('window:resize', ['$event'])
resizeMap() {
this.mapWidth = window.innerWidth;
this.mapHeight = window.innerHeight;
this.changeDetectorRef.detectChanges();
this.mapService.map.updateSize();
}
Angular screen orientation event catch
screen.orientation.addEventListener('change', () => {
const { type } = screen.orientation;
if (!type.startsWith('portrait')) {
Swal.fire(
this.l('Information'),
this.l('LandscapeOrientationNotRecommended'),
'info'
);
// alert(this.l('LandscapeOrientationNotRecommended'));
}
});
Custom Xamarin iOS WKWebView
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Source")
{
if (Element.Source != null)
{
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Source)));
}
}
}
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
this.AutoresizingMask = UIViewAutoresizing.FlexibleDimensions;
this.ContentMode = UIViewContentMode.ScaleToFill;
if (e.OldElement != null)
{
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
var hybridWebView = e.OldElement as CustomWebView;
hybridWebView.Cleanup();
}
if (e.NewElement != null)
{
if (Control == null)
{
userController = new WKUserContentController();
var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(this, "invokeAction");
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(Frame, config);
SetNativeControl(webView);
}
if (Element.Source != null)
{
string contentDirectoryPath = Path.Combine(NSBundle.MainBundle.BundlePath);
Control.LoadHtmlString(Element.Source, new NSUrl(contentDirectoryPath, true));
}
}
}
public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
if (message.Body != null)
{
Element.InvokeAction(message.Body.ToString());
}
}
}
}
If someone have any suggestions ?
Thanks for your help.
How to load more items in Listview from ViewModel ?
Code Implemented :
listview.ItemAppearing += (sender, e) =>
{
if(isLoading || Items.Count == 0)
return;
//hit bottom!
if(e.Item.ToString() == Items[Items.Count - 1])
{
LoadItems();
}
};
in my xaml.cs
But need to do the same in my ViewModel...
try the following thing & let me know if you need some more help.
I used MessagingCenter to achieve it.
In your xaml.cs file add this
public partial class Results : ContentPage
{
public Results()
{
InitializeComponent();
NavigationPage.SetBackButtonTitle(this, "");
listview.ItemAppearing += (object s, ItemVisibilityEventArgs e) =>
{
MessagingCenter.Send(this, "Search:LastResultShown", e);
};
}
}
Now subscribe to your MessagingCenter in your ViewModel & remember to unsubscribe it as well.
public class ResultsVM : ViewModelBase
{
public ResultsVM() : base()
{
MessagingCenter.Subscribe<Results, ItemVisibilityEventArgs>(this, "Search:LastResultShown", OnItemAppearing);
}
public override void CleanupPage()
{
base.CleanupPage();
MessagingCenter.Unsubscribe<Results, ItemVisibilityEventArgs>(this, "Search:LastResultShown");
}
private async void OnItemAppearing(object sender, ItemVisibilityEventArgs e)
{
//Here will be your code
if ( ResultsList.Count == 0 || e == null || e.Item == null ) { return; }
var lastItem = ResultsList.ElementAtOrDefault(ResultsList.Count - 1);
if ( lastItem != null && e.Item == lastItem ) { LoadNextItems(); }
}
}
OnItemAppearing will hit everytime when your list comes at the end of screen.
In the past i've written a small renderer for buttons to maintain a padding property on my forms element. Lately it stopped working and while debugging i noticed it says unknown member all of a sudden (which would explain why it has no effect anymore)
[assembly: ExportRenderer(typeof(EnhancedButton), typeof(EnhancedButtonRenderer))]
namespace MyNamespace
{
public class EnhancedButtonRenderer : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
UpdatePadding();
}
private void UpdatePadding()
{
var element = this.Element as EnhancedButton;
if (element != null && this.Control != null)
{
this.Control.ContentEdgeInsets = new UIEdgeInsets(
(int)element.Padding.Top,
(int)element.Padding.Left,
(int)element.Padding.Bottom,
(int)element.Padding.Right
);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(EnhancedButton.Padding))
{
UpdatePadding();
}
}
}
}
pcl:
public class EnhancedButton : Button
{
#region Padding
public static BindableProperty PaddingProperty = BindableProperty.Create<EnhancedButton, Thickness>(d => d.Padding, default(Thickness));
public Thickness Padding
{
get { return (Thickness) GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
#endregion Padding
}
Is anyone aware of a workaround? Has the support of this property been canceled on ios side?
Xamarin.Forms is 2.1.0.6524. If i recall correctly it worked just fine a couple versions ago.
I wish to make two ListBoxes scroll together.
I have two ListBoxes of the same height with the same number of items, etc. I want to set it up such that if the user scrolls up/down in one list box the scrollbar for the other ListBox scrolls up/down as well.
But I can not seem to find a way to either detect the scroll bar position value or to detect when it has changed value.
Here is another way to sync the two ListBoxes:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace SyncTwoListBox
{
public partial class Form1 : Form
{
private SyncListBoxes _SyncListBoxes = null;
public Form1()
{
InitializeComponent();
this.Load += Form1_Load;
//add options
for (int i = 0; i < 40; i++)
{
listBox1.Items.Add("Item " + i);
listBox2.Items.Add("Item " + i);
}
}
private void Form1_Load(object sender, EventArgs e)
{
this._SyncListBoxes = new SyncListBoxes(this.listBox1, this.listBox2);
}
}
public class SyncListBoxes
{
private ListBox _LB1 = null;
private ListBox _LB2 = null;
private ListBoxScroll _ListBoxScroll1 = null;
private ListBoxScroll _ListBoxScroll2 = null;
public SyncListBoxes(ListBox LB1, ListBox LB2)
{
if (LB1 != null && LB1.IsHandleCreated && LB2 != null && LB2.IsHandleCreated &&
LB1.Items.Count == LB2.Items.Count && LB1.Height == LB2.Height)
{
this._LB1 = LB1;
this._ListBoxScroll1 = new ListBoxScroll(LB1);
this._ListBoxScroll1.Scroll += _ListBoxScroll1_VerticalScroll;
this._LB2 = LB2;
this._ListBoxScroll2 = new ListBoxScroll(LB2);
this._ListBoxScroll2.Scroll += _ListBoxScroll2_VerticalScroll;
this._LB1.SelectedIndexChanged += _LB1_SelectedIndexChanged;
this._LB2.SelectedIndexChanged += _LB2_SelectedIndexChanged;
}
}
private void _LB1_SelectedIndexChanged(object sender, EventArgs e)
{
if (this._LB2.TopIndex != this._LB1.TopIndex)
{
this._LB2.TopIndex = this._LB1.TopIndex;
}
if (this._LB2.SelectedIndex != this._LB1.SelectedIndex)
{
this._LB2.SelectedIndex = this._LB1.SelectedIndex;
}
}
private void _LB2_SelectedIndexChanged(object sender, EventArgs e)
{
if (this._LB1.TopIndex != this._LB2.TopIndex)
{
this._LB1.TopIndex = this._LB2.TopIndex;
}
if (this._LB1.SelectedIndex != this._LB2.SelectedIndex)
{
this._LB1.SelectedIndex = this._LB2.SelectedIndex;
}
}
private void _ListBoxScroll1_VerticalScroll(ListBox LB)
{
if (this._LB2.TopIndex != this._LB1.TopIndex)
{
this._LB2.TopIndex = this._LB1.TopIndex;
}
}
private void _ListBoxScroll2_VerticalScroll(ListBox LB)
{
if (this._LB1.TopIndex != this._LB2.TopIndex)
{
this._LB1.TopIndex = this._LB2.TopIndex;
}
}
private class ListBoxScroll : NativeWindow
{
private ListBox _LB = null;
private const int WM_VSCROLL = 0x115;
private const int WM_MOUSEWHEEL = 0x20a;
public event dlgListBoxScroll Scroll;
public delegate void dlgListBoxScroll(ListBox LB);
public ListBoxScroll(ListBox LB)
{
this._LB = LB;
this.AssignHandle(LB.Handle);
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
switch (m.Msg)
{
case WM_VSCROLL:
case WM_MOUSEWHEEL:
if (this.Scroll != null)
{
this.Scroll(_LB);
}
break;
}
}
}
}
}
enter image description here