Related
I am migrating our app from Xamarin to MAUI, and I am a bit struggling with migrating the code that handles JS/.NET interactions in a WebView on both Android and iOS. Let's focus on Android. It's especially about calling .NET code from JS in the WebView.
In Xamarin, we could do something like this (basically according to this tutorial https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/hybridwebview):
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface("jsBridge");
}
if (e.NewElement != null)
{
Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
Control.AddJavascriptInterface(new JsBridge(this), "jsBridge");
}
}
and
public class JavascriptWebViewClient : FormsWebViewClient
{
private readonly string javascript;
public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
{
this.javascript = javascript;
}
public override void OnPageFinished(WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(javascript, null);
}
}
In .NET 6 with MAUI, this is deprecated. I tried to build it with handlers, but then the OnPageFinished is never called. The lack of examples is making it difficult to figure out what I miss.
Microsoft.Maui.Handlers.WebViewHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
handler.PlatformView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
#endif
});
with
public class JavascriptWebViewClient : WebViewClient
{
private readonly string javascript;
public JavascriptWebViewClient(string javascript) : base()
{
this.javascript = javascript;
}
public override void OnPageFinished(WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(javascript, null);
}
}
Where should I put this code? Is this the correct way? What am I missing? I now put this in a subclassed WebView, but probably that's not the right way.
Update: I developed a work-around for Windows. See below.
TL;DR -
https://github.com/nmoschkin/MAUIWebViewExample
I have come up with a MAUI solution that work for both iOS and Android, using the new Handler pattern as described in:
Porting Custom Renderers To Handlers
The above documentation was somewhat poor, and did not feature an implementation for the iOS version. I provide that, here.
This adaptation also makes the Source property a BindableProperty. Unlike the example in the above link, I do not actually add the property to the PropertyMapper in the platform handler in the traditional way. Rather, we will be listening for an event to be fired by the property changed notification method of the bindable property.
This example implements a 100% custom WebView. If there are additional properties and methods you would like to port over from the native components, you will have to add that additional functionality, yourself.
Shared Code:
In the shared code file, you want to create your custom view by implementing the classes and interface as described in the above link in the following way (with additional classes provided for events that we will provide to the consumer):
public class SourceChangedEventArgs : EventArgs
{
public WebViewSource Source
{
get;
private set;
}
public SourceChangedEventArgs(WebViewSource source)
{
Source = source;
}
}
public class JavaScriptActionEventArgs : EventArgs
{
public string Payload { get; private set; }
public JavaScriptActionEventArgs(string payload)
{
Payload = payload;
}
}
public interface IHybridWebView : IView
{
event EventHandler<SourceChangedEventArgs> SourceChanged;
event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
void Refresh();
WebViewSource Source { get; set; }
void Cleanup();
void InvokeAction(string data);
}
public class HybridWebView : View, IHybridWebView
{
public event EventHandler<SourceChangedEventArgs> SourceChanged;
public event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
public HybridWebView()
{
}
public void Refresh()
{
if (Source == null) return;
var s = Source;
Source = null;
Source = s;
}
public WebViewSource Source
{
get { return (WebViewSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly BindableProperty SourceProperty = BindableProperty.Create(
propertyName: "Source",
returnType: typeof(WebViewSource),
declaringType: typeof(HybridWebView),
defaultValue: new UrlWebViewSource() { Url = "about:blank" },
propertyChanged: OnSourceChanged);
private static void OnSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as HybridWebView;
bindable.Dispatcher.Dispatch(() =>
{
view.SourceChanged?.Invoke(view, new SourceChangedEventArgs(newValue as WebViewSource));
});
}
public void Cleanup()
{
JavaScriptAction = null;
}
public void InvokeAction(string data)
{
JavaScriptAction?.Invoke(this, new JavaScriptActionEventArgs(data));
}
}
Then you would have to declare the handler for each platform, as follows:
Android Implementation:
public class HybridWebViewHandler : ViewHandler<IHybridWebView, Android.Webkit.WebView>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
private JSBridge jsBridgeHandler;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
}
private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override Android.Webkit.WebView CreatePlatformView()
{
var webView = new Android.Webkit.WebView(Context);
jsBridgeHandler = new JSBridge(this);
webView.Settings.JavaScriptEnabled = true;
webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
webView.AddJavascriptInterface(jsBridgeHandler, "jsBridge");
return webView;
}
protected override void ConnectHandler(Android.Webkit.WebView platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += VirtualView_SourceChanged;
}
protected override void DisconnectHandler(Android.Webkit.WebView platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= VirtualView_SourceChanged;
VirtualView.Cleanup();
jsBridgeHandler?.Dispose();
jsBridgeHandler = null;
}
private static void LoadSource(WebViewSource source, Android.Webkit.WebView control)
{
try
{
if (source is HtmlWebViewSource html)
{
control.LoadDataWithBaseURL(html.BaseUrl, html.Html, null, "charset=UTF-8", null);
}
else if (source is UrlWebViewSource url)
{
control.LoadUrl(url.Url);
}
}
catch { }
}
}
public class JavascriptWebViewClient : WebViewClient
{
string _javascript;
public JavascriptWebViewClient(string javascript)
{
_javascript = javascript;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
view.EvaluateJavascript(_javascript, null);
}
}
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
internal JSBridge(HybridWebViewHandler hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewHandler hybridRenderer;
if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
hybridRenderer.VirtualView.InvokeAction(data);
}
}
}
iOS Implementation:
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WKWebView>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
private WKUserContentController userController;
private JSBridge jsBridgeHandler;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
}
private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WKWebView CreatePlatformView()
{
jsBridgeHandler = new JSBridge(this);
userController = new WKUserContentController();
var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(jsBridgeHandler, "invokeAction");
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(CGRect.Empty, config);
return webView;
}
protected override void ConnectHandler(WKWebView platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += VirtualView_SourceChanged;
}
protected override void DisconnectHandler(WKWebView platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= VirtualView_SourceChanged;
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
jsBridgeHandler?.Dispose();
jsBridgeHandler = null;
}
private static void LoadSource(WebViewSource source, WKWebView control)
{
if (source is HtmlWebViewSource html)
{
control.LoadHtmlString(html.Html, new NSUrl(html.BaseUrl ?? "http://localhost", true));
}
else if (source is UrlWebViewSource url)
{
control.LoadRequest(new NSUrlRequest(new NSUrl(url.Url)));
}
}
}
public class JSBridge : NSObject, IWKScriptMessageHandler
{
readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
internal JSBridge(HybridWebViewHandler hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
}
public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
HybridWebViewHandler hybridRenderer;
if (hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
hybridRenderer.VirtualView?.InvokeAction(message.Body.ToString());
}
}
}
As you can see, I'm listening for the event to change out the source, which will then perform the platform-specific steps necessary to change it.
Also note that in both implementations of JSBridge I am using a WeakReference to track the control. I am not certain of any situations where disposal might deadlock, but I did this out of an abundance of caution.
Windows Implementation:
So. According to various articles I read, the current WinUI3 iteration of WebView2 for MAUI is not yet allowing us to invoke AddHostObjectToScript. They plan this for a future release.
But, then I remembered it was Windows, so I created a work-around that most certainly emulates the same behavior and achieves the same result, with a somewhat unorthodox solution: by using an HttpListener.
internal class HybridSocket
{
private HttpListener listener;
private HybridWebViewHandler handler;
bool token = false;
public HybridSocket(HybridWebViewHandler handler)
{
this.handler = handler;
CreateSocket();
}
private void CreateSocket()
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:32000/");
}
public void StopListening()
{
token = false;
}
private void SendToNative(string json)
{
handler.VirtualView.InvokeAction(json);
}
public void Listen()
{
var s = listener;
try
{
token = true;
s.Start();
while (token)
{
HttpListenerContext ctx = listener.GetContext();
using HttpListenerResponse resp = ctx.Response;
resp.AddHeader("Access-Control-Allow-Origin", "null");
resp.AddHeader("Access-Control-Allow-Headers", "content-type");
var req = ctx.Request;
Stream body = req.InputStream;
Encoding encoding = req.ContentEncoding;
using (StreamReader reader = new StreamReader(body, encoding))
{
var json = reader.ReadToEnd();
if (ctx.Request.HttpMethod == "POST")
{
SendToNative(json);
}
}
resp.StatusCode = (int)HttpStatusCode.OK;
resp.StatusDescription = "Status OK";
}
CreateSocket();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WebView2>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = #"function invokeCSharpAction(data)
{
var http = new XMLHttpRequest();
var url = 'http://localhost:32000';
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/json');
http.send(JSON.stringify(data));
}";
static SynchronizationContext sync;
private HybridSocket jssocket;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
sync = SynchronizationContext.Current;
jssocket = new HybridSocket(this);
Task.Run(() => jssocket.Listen());
}
~HybridWebViewHandler()
{
jssocket.StopListening();
}
private void OnWebSourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WebView2 CreatePlatformView()
{
sync = sync ?? SynchronizationContext.Current;
var webView = new WebView2();
webView.NavigationCompleted += WebView_NavigationCompleted;
return webView;
}
private void WebView_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
var req = new EvaluateJavaScriptAsyncRequest(JavascriptFunction);
PlatformView.EvaluateJavaScript(req);
}
protected override void ConnectHandler(WebView2 platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += OnWebSourceChanged;
}
protected override void DisconnectHandler(WebView2 platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= OnWebSourceChanged;
VirtualView.Cleanup();
}
private static void LoadSource(WebViewSource source, WebView2 control)
{
try
{
if (control.CoreWebView2 == null)
{
control.EnsureCoreWebView2Async().AsTask().ContinueWith((t) =>
{
sync.Post((o) => LoadSource(source, control), null);
});
}
else
{
if (source is HtmlWebViewSource html)
{
control.CoreWebView2.NavigateToString(html.Html);
}
else if (source is UrlWebViewSource url)
{
control.CoreWebView2.Navigate(url.Url);
}
}
}
catch { }
}
}
Finally, you will need to initialize the MAUI application by adding ConfigureMauiHandlers to the app builder:
Initialize the MAUI Application in MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(HybridWebView), typeof(HybridWebViewHandler));
});
return builder.Build();
}
Add The Control To XAML
<controls:HybridWebView
x:Name="MyWebView"
HeightRequest="128"
HorizontalOptions="Fill"
Source="{Binding Source}"
VerticalOptions="FillAndExpand"
WidthRequest="512"
/>
Finally, I have added all of the above to a full example MAUI project in a repository on GitHub:
https://github.com/nmoschkin/MAUIWebViewExample
The GitHub repo example also includes a ViewModel that contains the WebViewSource to which the control is bound in markup.
OK, figured it out. Adding information for those looking to the same problem.
What you need to do:
Override WebView client.
Generic:
public partial class CustomWebView : WebView
{
partial void ChangedHandler(object sender);
partial void ChangingHandler(object sender, HandlerChangingEventArgs e);
protected override void OnHandlerChanging(HandlerChangingEventArgs args)
{
base.OnHandlerChanging(args);
ChangingHandler(this, args);
}
protected override void OnHandlerChanged()
{
base.OnHandlerChanged();
ChangedHandler(this);
}
public void InvokeAction(string data)
{
// your custom code
}
}
Android:
public partial class CustomWebView
{
const string JavascriptFunction = "function invokeActionInCS(data){jsBridge.invokeAction(data);}";
partial void ChangedHandler(object sender)
{
if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
nativeWebView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
nativeWebView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
}
partial void ChangingHandler(object sender, HandlerChangingEventArgs e)
{
if (e.OldHandler != null)
{
if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
nativeWebView.RemoveJavascriptInterface("jsBridge");
}
}
}
Add this custom view to your XAML
<views:CustomWebView x:Name="CustomWebViewName"/>
Modify the JS Bridge
public class JsBridge : Java.Lang.Object
{
private readonly HarmonyWebView webView;
public JsBridge(HarmonyWebView webView)
{
this.webView = webView;
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
webView.InvokeAction(data);
}
}
I solved it finnaly. It's not completely solution however its work well.
First, we define 2 variables globally on the javascipt side and define 2 functions waiting for each other.
var _flagformobiledata = false;
var _dataformobiledata = "";
const waitUntilMobileData = (condition, checkInterval = 100) => {
return new Promise(resolve => {
let interval = setInterval(() => {
if (!condition()) return;
clearInterval(interval);
resolve();
}, checkInterval)
})
}
async function mobileajax(functionName, params) {
window.location.href = "https://runcsharp." + functionName + "?" + params;
await waitUntilMobileData(() => _flagformobiledata == true);
_flagformobiledata = false;
return _dataformobiledata;
}
function setmobiledata(aData) {
_dataformobiledata = aData;
_flagformobiledata = true;
}
Then in MainPage.xaml.cs file define a function named WebViewNavigation
private async void WebViewNavigation(object sender, WebNavigatingEventArgs e)
{
var urlParts = e.Url.Split("runcsharp.");
if (urlParts.Length == 2)
{
Console.WriteLine(urlParts);
var funcToCall = urlParts[1].Split("?");
var methodName = funcToCall[0];
var funcParams = funcToCall[1];
e.Cancel = true;
if (methodName.Contains("login"))
{
var phonenumber = funcParams.Split("=");
var ActualPhoneNumber = "";
if (phonenumber.Length == 2)
{
ActualPhoneNumber = Regex.Replace(phonenumber[1].Replace("%20", "").ToString(), #"[^\d]", "");
}
var response = _authService.GetMobileLicanceInfo(ActualPhoneNumber);
if (response.status == 200)
{
PhoneGlobal = ActualPhoneNumber;
string maui = "setmobiledata('" + "OK" + "')"; // this is function to set global return data
await CustomWebView.EvaluateJavaScriptAsync(maui); // then run the script
}
else
{
await DisplayAlert("Error", response.message, "OK");
}
}
}
}
Then your Mainpage.xaml file:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DinamoMobile.MainPage">
<WebView Navigating="WebViewNavigation" x:Name="CustomWebView">
</WebView>
</ContentPage>
After all that coding usage must be like:
<script>const login = document.querySelector(".js-login-btnCls");
login.addEventListener("click", logincallapi);
async function logincallapi() {
var phone = $("#phone").val();
if (phone == "") {
alert("Phone is required");
return;
}
var isOK = await mobileajax("login", "phone=" + phone);
if (isOK == "OK") {
window.location.href = "verification.html";
}
else {
alert("Invalid Phone Number.");
}
}</script>
Algorithm:
Write an asynchronous function that waits for data on the javascript side.
Go with the URL of the desired function on the Javascript side.
Set data to global variable with EvulateJavascript function.
The function waiting for the data will continue in this way.
I have a base class from System.Windows.Controls.Control that changes Visibility, Enabled , Background, Foreground properties according to data from outside.
when I use the class like below
public class RsdDesignBase : Button
{
....
}
It works for Button Control. I want to use same class for other controls like TextBox, Image, TextBlock but if I use like this I neet copy paste same code for all other controls.
Is there a way to use my RsdDesignBase class as base class for others controls ? Or any other way to do this.
I will paste whole class below. What it does is waits for changes in DataTag objects when they change it changes to some properties. For example if _enabledTag.Value is 0 it disables the control.
public class RsdDesignButtonBase : Button
{
private DataTag _visibilityTag;
private DataTag _enabledTag;
private DataTag _appearanceTag;
public TagScriptObject TagScriptObject { get; set; }
private readonly Timer _timer;
protected RsdDesignButtonBase()
{
Loaded += RSD_ButtonBase_Loaded;
Unloaded += OnUnloaded;
_timer = new Timer(1000);
_timer.Elapsed += TimerOnElapsed;
}
private void TimerOnElapsed(object sender, ElapsedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
var background = Background;
var foreground = Foreground;
Background = foreground;
Foreground = background;
}), DispatcherPriority.Render);
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
if (_enabledTag != null) _enabledTag.DataChanged -= EnabledTagOnDataChanged;
if (_visibilityTag != null) _visibilityTag.DataChanged -= VisibilityTagOnDataChanged;
if (_appearanceTag != null) _appearanceTag.DataChanged -= AppearanceTagOnDataChanged;
}
private void RSD_ButtonBase_Loaded(object sender, RoutedEventArgs e)
{
DependencyPropertyDescriptor desc =
DependencyPropertyDescriptor.FromProperty(FrameworkElement.TagProperty, typeof(FrameworkElement));
desc.AddValueChanged(this, TagPropertyChanged);
TagPropertyChanged(null, null);
}
private void TagPropertyChanged(object sender, EventArgs e)
{
if (Tag == null) return;
TagScriptObject = JsonConvert.DeserializeObject<TagScriptObject>(Tag.ToString());
if (TagScriptObject?.VisibilityProperty?.TagId > 0)
{
_visibilityTag =
GlobalVars.AllDataTagList.FirstOrDefault(t => t.Id == TagScriptObject.VisibilityProperty?.TagId);
if (_visibilityTag != null)
{
_visibilityTag.DataChanged += VisibilityTagOnDataChanged;
VisibilityTagOnDataChanged(null, null);
}
}
if (TagScriptObject?.EnableProperty?.TagId > 0)
{
_enabledTag =
GlobalVars.AllDataTagList.FirstOrDefault(t => t.Id == TagScriptObject.EnableProperty?.TagId);
if (_enabledTag != null)
{
_enabledTag.DataChanged += EnabledTagOnDataChanged;
EnabledTagOnDataChanged(null, null);
}
}
if (TagScriptObject?.AppearanceProperty?.TagId > 0)
{
_appearanceTag =
GlobalVars.AllDataTagList.FirstOrDefault(t => t.Id == TagScriptObject.AppearanceProperty?.TagId);
if (_appearanceTag != null && !_appearanceTag.IsEventHandlerRegistered(null))
{
_appearanceTag.DataChanged += AppearanceTagOnDataChanged;
AppearanceTagOnDataChanged(null, null);
}
}
}
private void AppearanceTagOnDataChanged(object source, EventArgs args)
{
_timer.Enabled = false;
_ = Dispatcher.BeginInvoke(new Action(() =>
{
double tagValue;
bool result = true;
if (_appearanceTag.VarType == VarType.Bit)
{
tagValue = _appearanceTag.TagValue ? 1 : 0;
}
else
{
result = double.TryParse(_appearanceTag.TagValue.ToString(), out tagValue);
}
if (result)
{
foreach (var controlColor in TagScriptObject.AppearanceProperty.ControlColors)
{
if (tagValue >= controlColor.RangeMin &&
tagValue <= controlColor.RangeMax)
{
Background =
new BrushConverter().ConvertFromString(controlColor.Background) as SolidColorBrush;
Foreground =
new BrushConverter().ConvertFromString(controlColor.Foreground) as SolidColorBrush;
_timer.Enabled = controlColor.Flashing == ConfirmEnum.Yes;
break;
}
}
}
}), DispatcherPriority.Render);
}
private void EnabledTagOnDataChanged(object source, EventArgs args)
{
_ = Dispatcher.BeginInvoke(new Action(() =>
{
if (_enabledTag != null)
{
if (TagScriptObject.EnableProperty.IsRangeSelected)
{
double tagValue;
bool result = true;
if (_enabledTag.VarType == VarType.Bit)
{
tagValue = _enabledTag.TagValue ? 1 : 0;
}
else
{
result = double.TryParse(_enabledTag.TagValue.ToString(), out tagValue);
}
if (result)
{
if (tagValue >= TagScriptObject.EnableProperty.RangeFrom &&
tagValue <= TagScriptObject.EnableProperty.RangeTo)
{
IsEnabled = TagScriptObject.EnableProperty.DefaultValue;
}
else
{
IsEnabled = !TagScriptObject.EnableProperty.DefaultValue;
}
}
}
else
{
if (_enabledTag.IsNumeric || _enabledTag.VarType == VarType.Bit)
{
var bitArray = _enabledTag.GetBitArray();
var singleBit = TagScriptObject.EnableProperty.SingleBit;
if (bitArray.Count > singleBit)
{
if (bitArray[singleBit])
{
IsEnabled = TagScriptObject.EnableProperty.DefaultValue;
}
else
{
IsEnabled = !TagScriptObject.EnableProperty.DefaultValue;
}
}
}
}
}
}), DispatcherPriority.Render);
}
private void VisibilityTagOnDataChanged(object source, EventArgs args)
{
_ = Dispatcher.BeginInvoke(new Action(() =>
{
if (_visibilityTag != null)
{
if (TagScriptObject.VisibilityProperty.IsRangeSelected)
{
double tagValue;
bool result = true;
if (_visibilityTag.VarType == VarType.Bit)
{
tagValue = _visibilityTag.TagValue ? 1 : 0;
}
else
{
result = double.TryParse(_visibilityTag.TagValue.ToString(), out tagValue);
}
if (result)
{
if (tagValue >= TagScriptObject.VisibilityProperty.RangeFrom &&
tagValue <= TagScriptObject.VisibilityProperty.RangeTo)
{
Visibility = TagScriptObject.VisibilityProperty.DefaultValue
? Visibility.Visible
: Visibility.Hidden;
}
else
{
Visibility = TagScriptObject.VisibilityProperty.DefaultValue
? Visibility.Collapsed
: Visibility.Visible;
}
}
}
else
{
if (_visibilityTag.IsNumeric || _visibilityTag.VarType == VarType.Bit)
{
var bitArray = _visibilityTag.GetBitArray();
var singleBit = TagScriptObject.VisibilityProperty.SingleBit;
if (bitArray.Count > singleBit)
{
if (bitArray[singleBit])
{
Visibility = TagScriptObject.VisibilityProperty.DefaultValue
? Visibility.Visible
: Visibility.Hidden;
}
else
{
Visibility = TagScriptObject.VisibilityProperty.DefaultValue
? Visibility.Hidden
: Visibility.Visible;
}
}
}
}
}
}), DispatcherPriority.Render);
}
}
If I understand you correctly, you want to add some feature to Button, TextBox, Image and TextBlock (and possibly more) and reuse that code for all classes, right?
What you're doing right now is adding a Base at the bottom of the inheritance tree. That way you can't share it with other classes. Ideally, you would want to change the System.Windows.Controls.Control, but that's part of the .NET Framework, so you can't change that...
This is the downside of inheritance...
The only possibility I see is to use composition:
Create a class containing the logic you want. Let's call it RsdDesign. No superclass needed. It will look a lot like your RsdDesignButtonBase.
Create a descendant for every Control you want to add this feature to
Give those descendants a private member of type ``RsdDesign````.
Connect all applicable methods of the Control to the member.
public class RsdDesign
{
private DataTag _visibilityTag;
private DataTag _enabledTag;
private DataTag _appearanceTag;
public TagScriptObject TagScriptObject { get; set; }
private readonly Timer _timer;
private System.Windows.Controls.Control _parentControl
protected RsdDesign(System.Windows.Controls.Control parentControl)
{
_parentControl = parentControl;
_parentControl.Loaded += RSD_ButtonBase_Loaded;
_parentControl.Unloaded += OnUnloaded;
_timer = new Timer(1000);
_timer.Elapsed += TimerOnElapsed;
}
// The rest of your RsdDesignButtonBase implementation
// ...
}
public class RsdDesignButton: Button
{
private RsdDesign _design;
public RsdDesignButton(...)
{
_design = new RsdDesign(this);
}
// You may need to hook some of the methods explicitly like this:
private void EnabledTagOnDataChanged(object source, EventArgs args)
{
_design.EnabledTagOnDataChanged(source, args);
}
}
I haven't tried this, but maybe the idea helps you to find a solution.
If you derive from your RsdDesignButtonBase class from FrameworkElement:
public class RsdDesignBase : FrameworkElement
{
...
}
...you should be able to extend and customize it for TextBox, Image, TextBlock and any other FrameworkElement, e.g.:
public class TextBlock : RsdDesignBase {}
As far as I can see your control does two(three) things:
It sets a certain layout to the control (visibility, background etc)
it deals a lot with (de)serializing and processing JSON data.
Some of the processing in return modifies UI properties (e.g. Hide/Show) if certain data is available or not.
Following the helpful principal of "separation of concerns" - not because it sound academic or is 'awesome', but because you don't get into a mess of too tightly coupled code - I would much rather recommend to put all of this logic into an Attached Property or a set of Attached properties. And to pass the control as the first argument.
You would not have to change a lot of the implementation and you could use it for virtually all WPF elements that derive from Control or even FrameworkElement
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/attached-properties-overview?view=netframeworkdesktop-4.8
I'm working on a Xamarin.Forms app where I need to integrate a WebView to manage booking through an external URL.
So basically I've did this in my view:
<WebView x:Name="webView" Source="{Binding BookingUrl}"
WidthRequest="1000" HeightRequest="1000">
I would like to manage some errors that the users could encounter while opening this page: no internet access, timeout, unavailable server,...
For this I've used EventToCommandBehaviorto acess to the events Navigating and Navigating in the ViewModel.
So my XAML looks like this:
<WebView x:Name="webView" Source="{Binding AvilaUrlBooking}"
WidthRequest="1000" HeightRequest="1000">
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
Command="{Binding NavigatingCommand}" />
<behaviors:EventToCommandBehavior
EventName="Navigated"
Command="{Binding NavigatedCommand}" />
</WebView.Behaviors>
</WebView>
And the ViewModel is like this:
public ICommand NavigatingCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatingAsync(x);
}
});
}
}
private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
{
if (!IsConnected)
ServiceErrorKind = ServiceErrorKind.NoInternetAccess;
IsBusy = true;
return Task.CompletedTask;
}
public ICommand NavigatedCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatedAsync(x);
}
});
}
}
private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
{
IsBusy = false;
IsFirstDisplay = false;
switch (eventArgs.Result)
{
case WebNavigationResult.Cancel:
// TODO - do stuff here
break;
case WebNavigationResult.Failure:
// TODO - do stuff here
break;
case WebNavigationResult.Success:
// TODO - do stuff here
break;
case WebNavigationResult.Timeout:
// TODO - do stuff here
break;
default:
// TODO - do stuff here
break;
}
return Task.CompletedTask;
}
bool isFirstDisplay;
public bool IsFirstDisplay
{
get { return isFirstDisplay; }
set { SetProperty(ref isFirstDisplay, value); }
}
public BookingViewModel()
{
_eventTracker = new AppCenterEventTracker();
IsFirstDisplay = true;
Title = "Booking";
IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
Connectivity.ConnectivityChanged += OnConnectivityChanged;
}
If I use the right URL, all works fine on iOS and Android.
However, if I use a "wrong" URL (with missing char for example), this is only working on Android: the case WebNavigationResult.Failure is catched in WebViewNavigatedAsync(), but I don't enter in WebViewNavigatedAsync() on iOS.
=> is this normal?
I've implemented a "Refresh" button to manage the "No Internet access" error. This button is accessible through a ToolBarItem, it's like this in the ViewModel:
public void Refresh(object sender)
{
try
{
var view = sender as Xamarin.Forms.WebView;
view.Reload();
}
catch (Exception ex)
{
}
}
But in these case too, I have 2 different behaviours after having activated the Airplane mode:
on iOS, when there is no internet access: I don't enter in WebViewNavigatedAsync(), even if the internet access is available again and I click on the "Refresh" button, I only pass by the WebViewNavigatingAsync()
on Android, when there is no internet access: I well enter in WebViewNavigatedAsync(), and when the internet access is available again and I click on the "Refresh" button, I pass both by the WebViewNavigatingAsync() and WebViewNavigatedAsync()
=> is this normal? Is there a proper way to manager this?
is this normal?
Based on my test. Yes, I got the same result, if we input an error url, the webview always be white-empty view in iOS. so the NavigatedCommand cannot be executed.
If we use correct url, the webview could excute the NavigatedCommand, and running result like following screenshot.
Is there a proper way to manager this?
We can use custom renderer for webview in iOS to handle this situation.
[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace MyCarsourlView.iOS
{
[Obsolete]
class CustomWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement == null) { Delegate = new CustomWebViewDelegate(); }
}
}
}
internal class CustomWebViewDelegate : UIWebViewDelegate
{
#region Event Handlers
public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType)
{
//Could add stuff here to redirect the user before the page loads, if you wanted to redirect the user you would do the redirection and then return false
return true;
}
public override void LoadFailed(UIWebView webView, NSError error)
{
Console.WriteLine("\nIn AppName.iOS.CustomWebViewRenderer - Error: {0}\n", error.ToString()); //TODO: Do something more useful here
//Here, you can test to see what kind of error you are getting and act accordingly, either by showing an alert or rendering your own custom error page using basic HTML
}
public override void LoadingFinished(UIWebView webView)
{ //You could do stuff here such as collection cookies form the WebView }
#endregion
}
}
If I input wrong url, LoadFailed could be executed.
I've found another approach that seems to work, based on the following links:
It's probably not perfect, especially as I need access to the required Events from the ViewModel.
So I've created a CustomWebView control that inherits from WebView:
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
public CustomWebViewErrorKind ErrorKind { get; set; }
public event EventHandler LoadingStart;
public event EventHandler LoadingFinished;
public event EventHandler LoadingFailed;
/// <summary>
/// The event handler for refreshing the page
/// </summary>
public EventHandler OnRefresh { get; set; }
public void InvokeCompleted()
{
if (this.LoadingFinished != null)
{
ErrorKind = WebViewErrorKind.None;
this.LoadingFinished.Invoke(this, null);
}
}
public void InvokeStarted()
{
if (this.LoadingStart != null)
{
ErrorKind = WebViewErrorKind.None;
this.LoadingStart.Invoke(this, null);
}
}
public void InvokeFailed(CustomWebViewErrorKind errorKind)
{
if (this.LoadingFailed != null)
{
ErrorKind = errorKind;
this.LoadingFailed.Invoke(this, null);
}
}
/// <summary>
/// Refreshes the current page
/// </summary>
public void Refresh()
{
OnRefresh?.Invoke(this, new EventArgs());
}
}
Then I've the CustomWkWebViewRenderer that customizes the behavior of the CustomWebView:
[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWkWebViewRenderer))]
namespace MyProject.iOS.Renderers
{
public class CustomWkWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
public CustomWkWebViewRenderer()
{
Debug.WriteLine($"CustomWkWebViewRenderer - Ctor");
}
WKWebView webView;
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged()");
if (Control == null)
{
Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - Control == null");
webView = new WKWebView(Frame, new WKWebViewConfiguration()
{
MediaPlaybackRequiresUserAction = false
});
webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
SetNativeControl(webView);
Element.OnRefresh += (sender, ea) => Refresh(sender);
}
if (e.NewElement != null)
{
Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - e.NewElement != null");
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
SetNativeControl(webView);
}
}
private void Refresh(object sender)
{
Debug.WriteLine($"CustomWkWebViewRenderer - Refresh()");
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
}
}
}
I also have the CustomWkWebViewNavigationDelegate that implements the WKNavigationDelegate for this renderer:
public class CustomWkWebViewNavigationDelegate : WKNavigationDelegate
{
private CustomWebView element;
public CustomWkWebViewNavigationDelegate(CustomWebView element)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - Ctor");
this.element = element;
}
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFinishNavigation");
element.InvokeCompleted();
//base.DidFinishNavigation(webView, navigation);
}
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidStartProvisionalNavigation");
element.InvokeStarted();
//base.DidStartProvisionalNavigation(webView, navigation);
}
[Export("webView:didFailProvisionalNavigation:withError:")]
public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFailProvisionalNavigation - error : {error}");
var errorKind = CustomWebViewErrorKind.None;
switch (error.Code)
{
case -1009: // no internet access
{
errorKind = CustomWebViewErrorKind.NoInternetAccess;
break;
}
case -1001: // timeout
{
errorKind = CustomWebViewErrorKind.Timeout;
break;
}
case -1003: // server cannot be found
case -1100: // url not found on server
default:
{
errorKind = CustomWebViewErrorKind.Failure;
break;
}
}
element.InvokeFailed(errorKind);
//base.DidFailProvisionalNavigation(webView, navigation, error);
}
}
There is a CustomWebViewErrorKind enum that will allow me to implement a common error management in the ViewModel:
public enum CustomWebViewErrorKind
{
None = 0,
NoInternetAccess = 1,
Failure = 2,
Timeout = 3,
Cancel = 8,
Other = 9
}
To access to the Events from the ViewModel, I use a EventToCommandBehavior like described there
So, I've exposed all the Commands from the View like this:
<controls:CustomWebView x:Name="webView"
Source="{Binding MyUrlBooking}"
Uri="{Binding MyUrlBooking}"
WidthRequest="1000" HeightRequest="1000">
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
Command="{Binding NavigatingCommand}" />
<behaviors:EventToCommandBehavior
EventName="Navigated"
Command="{Binding NavigatedCommand}" />
<behaviors:EventToCommandBehavior
EventName="LoadingStart"
Command="{Binding LoadingStartCommand}" />
<behaviors:EventToCommandBehavior
EventName="LoadingFinished"
Command="{Binding LoadingFinishedCommand}" />
<behaviors:EventToCommandBehavior
EventName="LoadingFailed"
Command="{Binding LoadingFailedCommand}"
CommandParameter="{x:Reference webView}"
/>
</WebView.Behaviors>
</controls:CustomWebView>
And finally, in my ViewModel I do this for the Android part:
public ICommand NavigatingCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatingAsync(x);
}
});
}
}
private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatingAsync()");
IsBusy = true;
return Task.CompletedTask;
}
public ICommand NavigatedCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatedAsync(x);
}
});
}
}
private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync()");
IsBusy = false;
switch (eventArgs.Result)
{
case WebNavigationResult.Cancel:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Cancel");
ErrorKind = CustomWebViewErrorKind.Cancel;
break;
case WebNavigationResult.Failure:
default:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure");
IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
if (IsConnected)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : Failure");
ErrorKind = CustomWebViewErrorKind.Failure;
}
else
if (IsConnected)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : NoInternetAccess");
ErrorKind = CustomWebViewErrorKind.NoInternetAccess;
}
break;
case WebNavigationResult.Success:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Success");
ErrorKind = CustomWebViewErrorKind.None;
IsFirstDisplay = false;
break;
case WebNavigationResult.Timeout:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Timeout");
ErrorKind = CustomWebViewErrorKind.Timeout;
break;
}
return Task.CompletedTask;
}
And I do this for the iOS part:
public ICommand LoadingStartCommand
{
get
{
return new Xamarin.Forms.Command(async () =>
{
await WebViewLoadingStartAsync();
});
}
}
private Task WebViewLoadingStartAsync()
{
Debug.WriteLine($"BookingViewModel - WebViewLoadingStartAsync()");
IsBusy = true;
return Task.CompletedTask;
}
public ICommand LoadingFinishedCommand
{
get
{
return new Xamarin.Forms.Command(async () =>
{
await WebViewLoadingFinishedAsync();
});
}
}
private Task WebViewLoadingFinishedAsync()
{
Debug.WriteLine($"BookingViewModel - WebViewLoadingFinishedAsync()");
IsBusy = false;
return Task.CompletedTask;
}
public ICommand LoadingFailedCommand
{
get
{
return new Xamarin.Forms.Command<object>(async (object sender) =>
{
if (sender != null)
{
await WebViewLoadingFailedAsync(sender);
}
});
}
}
private Task WebViewLoadingFailedAsync(object sender)
{
Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync()");
var view = sender as CustomWebView;
var error = view.ErrorKind;
Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync() - error : {error}");
IsBusy = false;
return Task.CompletedTask;
}
Like this I'm able to manage errors, retry and refresh from the ViewModel, even if it's probably not the better solution...
I am building a Xamarin.Android application for a Zebra TC-70 Android device. My main activity runs the scanner just fine the first time through. It gets the data from the scanner and then passes data to another activity just fine. If I cancel the new activity or complete my work and return to the first activity, the scanner does not re-initialize. Here is my code:
[Activity(Label = "MyApp", MainLauncher = true, Icon = "#mipmap/icon",
ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : AppCompatActivity, EMDKManager.IEMDKListener
{
//EMDK
private BarcodeManager _barcodeManager;
private EMDKManager _emdkManager;
private EditText _scanBarcodeEditText;
private Scanner _scanner;
void EMDKManager.IEMDKListener.OnClosed()
{
if(_emdkManager != null)
{
_emdkManager.Release();
_emdkManager = null;
}
}
void EMDKManager.IEMDKListener.OnOpened(EMDKManager p0)
{
_emdkManager = p0;
InitScanner();
}
protected override void OnResume()
{
base.OnResume();
var intent = new Intent(this, typeof(FpmsDataService));
BindService(intent, _dataServiceConnection, Bind.AutoCreate);
_scanBarcodeEditText.Text = string.Empty;
InitScanner();
}
protected override void OnPause()
{
base.OnPause();
UnbindService(_dataServiceConnection);
DeinitScanner();
if (_emdkManager != null)
{
_emdkManager.Release();
_emdkManager = null;
}
}
protected override void OnDestroy()
{
base.OnDestroy();
DeinitScanner();
if(_emdkManager != null)
{
_emdkManager.Release();
_emdkManager = null;
}
Log.Information("Destroyed FirePMS MainActivity");
Log.CloseAndFlush();
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Window.SetFlags(WindowManagerFlags.KeepScreenOn, WindowManagerFlags.KeepScreenOn);
SetContentView(Resource.Layout.Main);
_scanBarcodeEditText = FindViewById<EditText>(Resource.Id.MaintenanceScanBarcode_EditText);
//Initialize Scanner
var results = EMDKManager.GetEMDKManager(Application.Context, this);
}
private void InitScanner()
{
if(_emdkManager != null)
{
if(_barcodeManager == null)
{
try
{
//Get the feature object such as BarcodeManager object for accessing the feature.
_barcodeManager = (BarcodeManager)_emdkManager.GetInstance(EMDKManager.FEATURE_TYPE.Barcode);
_scanner = _barcodeManager.GetDevice(BarcodeManager.DeviceIdentifier.Default);
if (_scanner != null)
{
//Attach the Data Event handler to get the data callbacks.
_scanner.Data += Scanner_Data;
_scanner.Status += Scanner_Status;
_scanner.Enable();
}
else
{
Log.Error("Failed to enable scanner");
}
}
catch (ScannerException e)
{
Log.Error(e.Message);
}
catch (Exception ex)
{
Log.Error(ex.Message);
}
}
}
}
private void Scanner_Status(object sender, Scanner.StatusEventArgs e)
{
var state = e.P0.State;
if(state == StatusData.ScannerStates.Idle)
{
try
{
if (_scanner.IsEnabled &&
!_scanner.IsReadPending)
{
SetScannerConfig();
_scanner.Read();
}
}
catch (ScannerException e1)
{
Log.Error(e1.Message);
}
}
}
private void DeinitScanner()
{
if(_emdkManager != null)
{
if(_scanner != null)
{
try
{
_scanner.CancelRead();
_scanner.Disable();
_scanner.Data -= Scanner_Data;
_scanner.Status -= Scanner_Status;
_scanner.Release();
}
catch (ScannerException e)
{
Log.Error(e.Result.Description);
}
}
}
if (_barcodeManager != null)
{
_emdkManager.Release(EMDKManager.FEATURE_TYPE.Barcode);
}
_barcodeManager = null;
_scanner = null;
}
private void SetScannerConfig()
{
var config = _scanner.GetConfig();
config.SkipOnUnsupported = ScannerConfig.SkipOnUnSupported.None;
config.ScanParams.DecodeLEDFeedback = true;
config.ReaderParams.ReaderSpecific.ImagerSpecific.PicklistEx = ScannerConfig.PicklistEx.Hardware;
config.DecoderParams.Code39.Enabled = true;
config.DecoderParams.Code128.Enabled = false;
_scanner.SetConfig(config);
}
private void Scanner_Data(object sender, Scanner.DataEventArgs e)
{
var scanDataCollection = e.P0;
if((scanDataCollection != null) && (scanDataCollection.Result == ScannerResults.Success))
{
var scanData = scanDataCollection.GetScanData();
if (scanData[0].Data == null)
{
return;
}
RunOnUiThread(() => _scanBarcodeEditText.Text = scanData[0].Data);
RunOnUiThread(ProcessScan);
}
}
private void ProcessScan()
{
if (string.IsNullOrEmpty(_scanBarcodeEditText.Text))
{
Toast.MakeText(this, "You must scan or enter a barcode to begin", ToastLength.Long).Show();
return;
}
else
{
var intent = new Intent(this, typeof(SecondActivity));
intent.PutExtra("ScannedData",_scanBarcodeEditText.Text);
StartActivity(intent)
}
}
}
Any suggestions would be greatly appreciated. As I indicated, the process works just fine the first time, it's when I return to this activity that the scanner is no longer initialized and doesn't come back.
I suspect the EMDK is not being properly released before the activity is restarted. If you take a look at the following article, though using Java the principle is the same and the advice there is to release the EMDK in onStop(). See the note about managing the EMDK instance about half way down.
Using C# 5.0, I'm creating a publish/subscribe relationship on a static field, so that I can access it from multiple pages. In the host window, I have
public enum PLCStates
{
Good,
Bad,
Disabled
};
public static class PLCSafeStates
{
public static event EventHandler testStates1Changed;
private static PLCStates _testStates1;
public static PLCStates testStates1
{
get { return _testStates1; }
set
{
if (value != _testStates1)
{
_testStates1 = value;
if (testStates1Changed != null)
testStates1Changed(null, EventArgs.Empty);
}
}
}
}
And then in the pages hosted by the window, I have things like:
public FB1()
{
InitializeComponent();
SafteyFaults.PLCSafeStates.testStates1Changed += PLCSafeStates_testStates1Changed;
}
private void PLCSafeStates_testStates1Changed(object sender, EventArgs e)
{
var test2 = SafteyFaults.PLCSafeStates.testStates1;
if (test2 == SafteyFaults.PLCStates.Bad)
{
VisualStateManager.GoToState(btnFB, "PLCBad", true);
}
if (test2 == SafteyFaults.PLCStates.Good)
{
VisualStateManager.GoToState(btnFB, "PLCGood", false);
}
}
private void btnFB_Click(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(btnOut1, "PLCBad",false);
if (SafteyFaults.PLCSafeStates.testStates1 == SafteyFaults.PLCStates.Good)
SafteyFaults.PLCSafeStates.testStates1=SafteyFaults.PLCStates.Bad;
else
SafteyFaults.PLCSafeStates.testStates1 = SafteyFaults.PLCStates.Good;
}
(right now, I don't have any business logic wired up yet- once I get this working, I'll link to actual data).
Anyhow, all of this works to create a single field I can subscribe to, modify, etc. But I need 20+ of these fields. I want to make 'testStates1' an array, but I've not been able to get it to work.
If I make the following edits to the code shown so far, it compiles and runs, but throws an error when I actually try to access the field (e.g. click on the button to change it):
//window
public static class PLCSafeStates
{
public static event EventHandler testStates1Changed;
private static PLCStates[] _testStates1;
public static PLCStates[] testStates1
{
get { return _testStates1; }
set
{
if (value != _testStates1)
{
_testStates1 = value;
if (testStates1Changed != null)
testStates1Changed(null, EventArgs.Empty);
}
}
}
}
//page
public FB1()
{
InitializeComponent();
SafteyFaults.PLCSafeStates.testStates1Changed += PLCSafeStates_testStates1Changed;
}
private void PLCSafeStates_testStates1Changed(object sender, EventArgs e)
{
var test2 = SafteyFaults.PLCSafeStates.testStates1[0];
if (test2 == SafteyFaults.PLCStates.Bad)
{
VisualStateManager.GoToState(btnFB, "PLCBad", true);
}
if (test2 == SafteyFaults.PLCStates.Good)
{
VisualStateManager.GoToState(btnFB, "PLCGood", false);
}
}
private void btnFB_Click(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(btnOut1, "PLCBad",false);
if (SafteyFaults.PLCSafeStates.testStates1[0] == SafteyFaults.PLCStates.Good)
SafteyFaults.PLCSafeStates.testStates1[0]=SafteyFaults.PLCStates.Bad;
else
SafteyFaults.PLCSafeStates.testStates1[0] = SafteyFaults.PLCStates.Good;
}