I want to make some methods of my API to be locked (HttpStatus.Conflict) until another one with same params not finished (like ?id=1&key=sd6gd0f1g5ds16fh), like if bad user try to make 2+ same request at once, only one will be done.
My idea was to use Semaphore:
public class Lock : IDisposable
{
private bool _disposed = false;
private readonly Semaphore _semaphore;
public bool IsLocked
{
get;
private set;
}
public Lock(string name)
{
this.IsLocked = false;
try
{
this._semaphore = Semaphore.OpenExisting(name);
this._semaphore.Close();
}
catch (Exception)
{
this._semaphore = new Semaphore(0, 1, name);
this.IsLocked = true;
}
}
~Lock()
{
this.Dispose(false);
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
this._semaphore.Release();
this._semaphore.Dispose();
}
this._disposed = true;
}
}
}
I'm using it like this:
[ActionName("Ping")]
[HttpGet]
public IHttpActionResult Ping([FromUri]int? id = null, [FromUri]string key = null)
{
if (id == null)
{
//ProcessException is some wrap for api error answer
throw new ProcessException(HttpStatusCode.BadRequest, "Service ID is required");
}
if (key == null)
{
throw new ProcessException(HttpStatusCode.BadRequest, "Service Key is required");
}
Lock serviceLock = new Lock("service." + id + "." + key);
if (!serviceLock.IsLocked)
{
throw new ProcessException(HttpStatusCode.Conflict, "Other Service operation already in progress");
}
var service = Service.Get((int)id, key);
if (service == null) // Right hereino
{
throw new ProcessException(HttpStatusCode.Forbidden, "Service ID and/or Key is invalid");
}
Service.Touch((int)id);
serviceLock.Dispose();
//JResponse is some wrap for Dictionary<string, object>
return Ok(new JResponse(true));
}
But I'm pretty new to it and have some questions:
Am I going in true direction?
When I'm calling Dispose, Semaphore still exists on next request. What's wrong?
Will my class be disposed (and Semaphore released) on some exception? (Like we can see above, if service == null)
This is not perfect and there is room for improvement but thought it might start you off an another direction or way to thinking.
Work your Semaphore stuff into locking the static dictionary
//ToDo: You would have to make this ThreadSafe
public static class Helper
{
public static Dictionary<string,ClientDto> ClientDtos
= new Dictionary<string, ClientDto>();
}
public class ClientDto
{
public int ClientKey { get; set; }
public string Key { get; set; }
public DateTime CreatedOn { get; set; }
}
in your Global.asax add.
protected void Application_EndRequest()
{
Helper.ClientDtos.Remove(SeesionId);
}
//if this is called twice by the same client and the request is
//not finished processing the first request the second one will go into
//RequestBeingHandled and just return preventing the code from preforming
//the same action until the first/current is complete.
public IHttpActionResult Ping([FromUri]int? id = null, [FromUri]string key = null)
{
if(RequestBeingHandled(id, key))
{
//
Return .....
}
else
{
//if not add
ClientDto client = new ClientDto();
client.ClientKey = id;
client.Key = key;
client.CreatedOn = DateTime.Now;
Helper.ClientDtos.Add(SeesionId, client);
}
//call some code to do stuff...
}
private bool RequestBeingHandled(int id, string key)
{
//ToDo: write this code.
//check if its already in the dic
return bool;
}
Related
My use case:
In a single threaded application, I need to serialize arbitrary classes for logging purposes.
The arbitrary classes are predominantly translated in an automated way from a massive VB6 application into .NET.
If serialized without a timeout, the serialization method will loop until it runs out of memory.
This is what I have currently:
internal class Serializer
{
private readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public volatile string result = null;
public volatile Func<string> toExecute = null;
public Thread thread;
public ManualResetEventSlim messageToSender = new ManualResetEventSlim(false);
public ManualResetEventSlim messageToReceiver = new ManualResetEventSlim(false);
public Serializer()
{
thread = new Thread(new ThreadStart(run));
thread.Start();
}
~Serializer()
{
try
{
if (messageToSender != null) messageToSender.Dispose();
}
catch { };
try
{
if (messageToReceiver != null) messageToReceiver.Dispose();
}
catch { };
}
public volatile bool ending = false;
public void run()
{
while (!ending)
{
try
{
if (toExecute != null)
{
result = toExecute();
}
messageToReceiver.Reset();
messageToSender.Set();
messageToReceiver.Wait();
}
catch (ThreadInterruptedException)
{
log.Warn("Serialization interrupted");
break;
}
catch (ThreadAbortException)
{
Thread.ResetAbort();
result = null;
}
catch (Exception ex)
{
log.Error("Error in Serialization", ex);
Console.WriteLine(ex);
break;
}
}
}
}
public class LocalStructuredLogging
{
private static volatile Serializer _serializer;
private static Serializer serializer
{
get
{
if (_serializer == null)
{
_serializer = new Serializer();
}
return _serializer;
}
}
public void LogStucturedEnd()
{
try
{
if (serializer != null)
{
serializer.ending = true;
serializer.thread.Interrupt();
}
}
catch { }
}
internal ConcurrentDictionary<long, bool> disallowedToSerialize = new ConcurrentDictionary<long, bool>();
public string TrySerialize<T>(T payload, [CallerLineNumber] int line = 0)
{
long hashEl = typeof(T).Name.GetHashCode() * line;
bool dummy;
unchecked
{
if (disallowedToSerialize.TryGetValue(hashEl, out dummy))
{
return "°,°";
}
}
serializer.toExecute = () =>
{
try
{
return Newtonsoft.Json.JsonConvert.SerializeObject(payload, new Newtonsoft.Json.JsonSerializerSettings() { ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore });
}
catch (Exception)
{
disallowedToSerialize.TryAdd(hashEl, false);
return "°°°";
}
};
try
{
serializer.messageToSender.Reset();
serializer.messageToReceiver.Set();
if (serializer.messageToSender.Wait(6000))
{
return Interlocked.Exchange(ref serializer.result, null);
}
serializer.toExecute = null;
serializer.thread.Abort();
serializer.messageToSender.Wait(2000);
disallowedToSerialize.TryAdd(hashEl, false);
return "°§°";
}
catch (Exception)
{
disallowedToSerialize.TryAdd(hashEl, false);
return "°-°";
}
}
}
The code is called as in the following (test is an arbitrary class instance):
var logger = new LocalStructuredLogging();
var rr5 = logger.TrySerialize(test);
Although it seems to do the job, there are some issues with it:
it has a dependency on Thread.Abort
it is time dependent, so it will thus produce varied results on a loaded system
every class instance is treated like every other class instance - no tweaking
...
So, are there any better solutions available ?
Based upon dbc's excellent answer, I managed to create a better timed serializer.
It resolves all 3 issues mentioned above:
public class TimedJsonTextWriter : JsonTextWriter
{
public int? MaxDepth { get; set; }
public TimeSpan? MaxTimeUsed { get; set; }
public int MaxObservedDepth { get; private set; }
private DateTime start = DateTime.Now;
public TimedJsonTextWriter(TextWriter writer, JsonSerializerSettings settings, TimeSpan? maxTimeUsed)
: base(writer)
{
this.MaxDepth = (settings == null ? null : settings.MaxDepth);
this.MaxObservedDepth = 0;
this.MaxTimeUsed = maxTimeUsed;
}
public TimedJsonTextWriter(TextWriter writer, TimeSpan? maxTimeUsed, int? maxDepth = null)
: base(writer)
{
this.MaxDepth = maxDepth;
this.MaxTimeUsed = maxTimeUsed;
}
public override void WriteStartArray()
{
base.WriteStartArray();
CheckDepth();
}
public override void WriteStartConstructor(string name)
{
base.WriteStartConstructor(name);
CheckDepth();
}
public override void WriteStartObject()
{
base.WriteStartObject();
CheckDepth();
}
uint checkDepthCounter = 0;
private void CheckDepth()
{
MaxObservedDepth = Math.Max(MaxObservedDepth, Top);
if (Top > MaxDepth)
throw new JsonSerializationException($"Depth {Top} Exceeds MaxDepth {MaxDepth} at path \"{Path}\"");
unchecked
{
if ((++checkDepthCounter & 0x3ff) == 0 && DateTime.Now - start > MaxTimeUsed)
throw new JsonSerializationException($"Time Usage Exceeded at path \"{Path}\"");
}
}
}
public class LocalStructuredLogging
{
public void LogStucturedEnd()
{
}
internal HashSet<long> disallowedToSerialize = new HashSet<long>();
public string TrySerialize<T>(T payload, int maxDepth = 100, int secondsToTimeout = 2, [CallerLineNumber] int line = 0)
{
long hashEl = typeof(T).Name.GetHashCode() * line;
if (disallowedToSerialize.Contains(hashEl))
{
return "°,°";
}
try
{
var settings = new JsonSerializerSettings { MaxDepth = maxDepth, ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore };
using (var writer = new StringWriter())
{
using (var jsonWriter = new TimedJsonTextWriter(writer, settings, new TimeSpan(0, 0, secondsToTimeout)))
{
JsonSerializer.Create(settings).Serialize(jsonWriter, payload);
// Log the MaxObservedDepth here, if you want to.
}
return writer.ToString();
}
}
catch (Exception)
{
disallowedToSerialize.Add(hashEl);
return "°-°";
}
}
}
The only issue remaining are the Hash collisions, which are easy to solve (e.g. by using the source file name as well or use another type of Collection).
The correct way to run an action timed would be to do something like the following. I would recommend taking a second look at how serialization should work as well :).
/// <summary>
/// Run an action timed.
/// </summary>
/// <param name="action">Action to execute timed.</param>
/// <param name="secondsTimout">Seconds before Task should cancel.</param>
/// <returns></returns>
public static async Task RunTimeout(Action action, int secondsTimout) {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(secondsTimout));
await Task.Run(action, tokenSource.Token);
}
You may also want to return a variable upon the completion of your timed task. That can be done like so...
public static async Task<T> RunTimeout<T>(Func<T> action, int secondsTimout) {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(secondsTimout));
var result = await Task.Run(action, tokenSource.Token);
return result;
}
I'm building an app which will scrape some data from a website and shows a notification when some criteria are met.
Everything works well without problems when the app is open (because the WebView is being rendered) but when I close the app the WebView is disabled so I cannot use it to scrape data anymore.
The scraping code is inside a class called from a ForegroundService.
I've already looked on the internet but I'm unable to find a solution or a substitute to WebView, do you have any ideas?
I'm sorry if this question looks stupid to you, I've started to develop for mobile just one week ago
Below the JDMonitoring class which is called from the AlarmTask class
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace CGSJDSportsNotification {
public class JDMonitoring {
class Ticket {
string owner;
string title;
string store;
string lastUpdated;
string link;
public string ID { get; set; }
public string Owner {
get {
return owner == null ? "Nobody" : owner;
} set {
owner = value.Remove(0, value.IndexOf('(') + 1).Replace(")", "");
}
}
public string Title {
get {
return title;
} set {
if (value.StartsWith("(P"))
title = value.Remove(0, value.IndexOf(')') + 2);
}
}
public string Status { get; set; }
public string Store {
get {
return store;
} set {
store = value.Replace(#"\u003C", "").Replace(">", "");
}
}
public string LastUpdated {
get {
return lastUpdated;
} set {
string v;
int time = Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(value, #"[^\d]+", ""));
// Convert to minutes
if (value.Contains("hours"))
time *= 60;
v = time.ToString();
if (value.Contains("seconds"))
v = v.Insert(v.Length, " sec. ago");
else
v = v.Insert(v.Length, " min. ago");
lastUpdated = v;
}
}
public string Link {
get {
return link;
} set {
link = "https://support.jdplc.com/" + value;
}
}
}
public JDMonitoring() {
WB.Source = JDQueueMainUrl;
WB.Navigated += new EventHandler<WebNavigatedEventArgs>(OnNavigate);
}
IForegroundService FgService { get { return DependencyService.Get<IForegroundService>(); } }
WebView WB { get; } = MainPage.UI.MonitoringWebView;
string JDQueueMainUrl { get; } = "https://support.jdplc.com/rt4/Search/Results.html?Format=%27%3Cb%3E%3Ca%20href%3D%22__WebPath__%2FTicket%2FDisplay.html%3Fid%3D__id__%22%3E__id__%3C%2Fa%3E%3C%2Fb%3E%2FTITLE%3A%23%27%2C%0A%27%3Cb%3E%3Ca%20href%3D%22__WebPath__%2FTicket%2FDisplay.html%3Fid%3D__id__%22%3E__Subject__%3C%2Fa%3E%3C%2Fb%3E%2FTITLE%3ASubject%27%2C%0AStatus%2C%0AQueueName%2C%0AOwner%2C%0APriority%2C%0A%27__NEWLINE__%27%2C%0A%27__NBSP__%27%2C%0A%27%3Csmall%3E__Requestors__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__CreatedRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__ToldRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__LastUpdatedRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__TimeLeft__%3C%2Fsmall%3E%27&Order=DESC%7CASC%7CASC%7CASC&OrderBy=LastUpdated%7C%7C%7C&Query=Queue%20%3D%20%27Service%20Desk%20-%20CGS%27%20AND%20(%20%20Status%20%3D%20%27new%27%20OR%20Status%20%3D%20%27open%27%20OR%20Status%20%3D%20%27stalled%27%20OR%20Status%20%3D%20%27deferred%27%20OR%20Status%20%3D%20%27open%20-%20awaiting%20requestor%27%20OR%20Status%20%3D%20%27open%20-%20awaiting%20third%20party%27%20)&RowsPerPage=0&SavedChartSearchId=new&SavedSearchId=new";
bool MonitoringIsInProgress { get; set; } = false;
public bool IsConnectionAvailable {
get {
try {
using (new WebClient().OpenRead("http://google.com/generate_204"))
return true;
} catch {
return false;
}
}
}
async Task<bool> IsOnLoginPage() {
if (await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('left')[0].innerText") != null)
return true;
return false;
}
async Task<bool> Login() {
await WB.EvaluateJavaScriptAsync($"document.getElementsByName('user')[0].value = '{UserSettings.SecureEntries.Get("rtUser")}'");
await WB.EvaluateJavaScriptAsync($"document.getElementsByName('pass')[0].value = '{UserSettings.SecureEntries.Get("rtPass")}'");
await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('button')[0].click()");
await Task.Delay(1000);
// Checks for wrong credentials error
if (await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('action-results')[0].innerText") == null)
return true;
return false;
}
async Task<List<Ticket>> GetTickets() {
List<Ticket> tkts = new List<Ticket>();
// Queue tkts index (multiple of 2)
int index = 2;
// Iterates all the queue
while (await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].innerText") != null) {
Ticket tkt = new Ticket();
tkt.LastUpdated = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index + 1}].getElementsByTagName('td')[4].innerText");
// Gets only the tkts which are not older than the value selected by the user
if (Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(tkt.LastUpdated, #"[^\d]+", "")) > Convert.ToInt32(UserSettings.Entries.Get("searchTimeframe")))
break;
tkt.ID = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[0].innerText");
tkt.Owner = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[4].innerText");
tkt.Title = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[1].innerText");
tkt.Status = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[2].innerText");
tkt.Store = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index + 1}].getElementsByTagName('td')[1].innerText");
tkt.Link = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[1].getElementsByTagName('a')[0].getAttribute('href')");
tkts.Add(tkt);
index += 2;
}
return tkts;
}
//async Task<string> QueueGetTkt
async void OnNavigate(object sender, WebNavigatedEventArgs args) {
if (MonitoringIsInProgress)
return;
if (IsConnectionAvailable) {
if (await IsOnLoginPage()) {
if (await Login() == false) {
// If the log-in failed we can't proceed
MonitoringIsInProgress = false;
FgService.NotificationNewTicket("Log-in failed!", "Please check your credentials");
// Used to avoid an infinite loop of OnNavigate method calls
WB.Source = "about:blank";
return;
}
}
// Main core of the monitoring
List<Ticket> tkts = await GetTickets();
if (tkts.Count > 0) {
foreach(Ticket t in tkts) {
// Looks only after the tkts with the country selected by the user (and if it was selected by the user, also for the tkts without a visible country)
// Firstly we look in the title
if (t.Title.Contains(MainPage.UI.CountryPicker.SelectedItem.ToString())) {
FgService.NotificationNewTicket($"[{t.ID}] {t.LastUpdated}",
$"{t.Title}\r\n\r\n" +
$"Status: {t.Status}\r\n" +
$"Owner: {t.Owner}\r\n" +
$"Last updated: {t.LastUpdated}");
break;
}
}
}
}
MonitoringIsInProgress = false;
}
}
}
AlarmTask class
using Android.App;
using Android.Content;
using Android.Support.V4.App;
namespace CGSJDSportsNotification.Droid {
[BroadcastReceiver(Enabled = true, Exported = true, DirectBootAware = true)]
[IntentFilter(new string[] { Intent.ActionBootCompleted, Intent.ActionLockedBootCompleted, "android.intent.action.QUICKBOOT_POWERON", "com.htc.intent.action.QUICKBOOT_POWERON" }, Priority = (int)IntentFilterPriority.HighPriority)]
public class AlarmTask : BroadcastReceiver {
IAlarm _MainActivity { get { return Xamarin.Forms.DependencyService.Get<IAlarm>(); } }
public override void OnReceive(Context context, Intent intent) {
if (intent.Action != null) {
if (intent.Action.Equals(Intent.ActionBootCompleted)) {
// Starts the app after reboot
var serviceIntent = new Intent(context, typeof(MainActivity));
serviceIntent.AddFlags(ActivityFlags.NewTask);
context.StartActivity(serviceIntent);
Intent main = new Intent(Intent.ActionMain);
main.AddCategory(Intent.CategoryHome);
context.StartActivity(main);
// Does not work, app crashes on boot received
/*if (UserSettings.Entries.Exists("monitoringIsRunning")) {
if ((bool)UserSettings.Entries.Get("monitoringIsRunning"))
FgService.Start();
}*/
}
} else
// Checks for new tkts on a new thread
new JDMonitoring();
// Restarts the alarm
_MainActivity.AlarmStart();
}
// Called from JDMonitoring class
public static void NotificationNewTicket(string title, string message, bool icoUnknownCountry = false) {
new AlarmTask().NotificationShow(title, message, icoUnknownCountry);
}
void NotificationShow(string title, string message, bool icoUnknownCountry) {
int countryFlag = Resource.Drawable.newTktUnknownCountry;
if (icoUnknownCountry == false) {
switch (MainPage.UI.CountryPicker.SelectedItem.ToString()) {
case "Italy":
countryFlag = Resource.Drawable.newTktItaly;
break;
case "Spain":
countryFlag = Resource.Drawable.newTktSpain;
break;
case "Germany":
countryFlag = Resource.Drawable.newTktGermany;
break;
case "Portugal":
countryFlag = Resource.Drawable.newTktPortugal;
break;
}
}
var _intent = new Intent(Application.Context, typeof(MainActivity));
_intent.AddFlags(ActivityFlags.ClearTop);
_intent.PutExtra("jdqueue_notification", "extra");
var pendingIntent = PendingIntent.GetActivity(Application.Context, 0, _intent, PendingIntentFlags.OneShot);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(Application.Context, "newTktNotification_channel")
.SetVisibility((int)NotificationVisibility.Public)
.SetPriority((int)NotificationPriority.High)
.SetDefaults((int)NotificationDefaults.Sound | (int)NotificationDefaults.Vibrate | (int)NotificationDefaults.Lights)
.SetSmallIcon(Resource.Drawable.newTktNotification)
.SetLargeIcon(Android.Graphics.BitmapFactory.DecodeResource(Application.Context.Resources, countryFlag))
.SetSubText("Click to check the queue")
.SetStyle(new NotificationCompat.BigTextStyle()
.SetBigContentTitle("New ticket available!")
.BigText(message))
.SetContentText(title)
.SetAutoCancel(true)
.SetContentIntent(pendingIntent);
NotificationManagerCompat.From(Application.Context).Notify(0, notificationBuilder.Build());
}
}
}
And the ForegroundService class which is responsible to trigger for the first time the alarm
using Android.App;
using Android.Content;
using Android.OS;
namespace CGSJDSportsNotification.Droid {
[Service]
class ForegroundService : Service {
IAlarm _MainActivity { get { return Xamarin.Forms.DependencyService.Get<IAlarm>(); } }
public override IBinder OnBind(Intent intent) { return null; }
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId) {
// Starts the Foreground Service and the notification channel
StartForeground(9869, new ForegroundServiceNotification().ReturnNotif());
Android.Widget.Toast.MakeText(Application.Context, "JD Queue - Monitoring started!", Android.Widget.ToastLength.Long).Show();
_MainActivity.AlarmStart();
return StartCommandResult.Sticky;
}
public override void OnDestroy() {
Android.Widget.Toast.MakeText(Application.Context, "JD Queue - Monitoring stopped!", Android.Widget.ToastLength.Long).Show();
_MainActivity.AlarmStop();
UserSettings.Entries.AddOrEdit("monitoringIsRunning", false);
UserSettings.Entries.AddOrEdit("monitoringStopPending", false, false);
base.OnDestroy();
}
public override bool StopService(Intent name) {
return base.StopService(name);
}
}
}
Thank you!
[BETTER-FINAL-SOLUTION]
After several hours I've discovered Android WebView which does exactly what I need (I'm developing this app only for Android)
I've written this Browser helper class
class Browser {
public Android.Webkit.WebView WB;
static string JSResult;
public class CustomWebViewClient : WebViewClient {
public event EventHandler<bool> OnPageLoaded;
public override void OnPageFinished(Android.Webkit.WebView view, string url) {
OnPageLoaded?.Invoke(this, true);
}
}
public Browser(CustomWebViewClient wc, string url = "") {
WB = new Android.Webkit.WebView(Android.App.Application.Context);
WB.Settings.JavaScriptEnabled = true;
WB.SetWebViewClient(wc);
WB.LoadUrl(url);
}
public string EvalJS(string js) {
JSInterface jsi = new JSInterface();
WB.EvaluateJavascript($"javascript:(function() {{ return {js}; }})()", jsi);
return JSResult;
}
class JSInterface : Java.Lang.Object, IValueCallback {
public void OnReceiveValue(Java.Lang.Object value) {
JSResult = value.ToString();
}
}
}
[EDIT]
Improved the JS returning function with async callbacks (so the JS return value will be always delivered).
Credits to ChristineZuckerman
class Browser {
public Android.Webkit.WebView WB;
public class CustomWebViewClient : WebViewClient {
public event EventHandler<bool> OnPageLoaded;
public override void OnPageFinished(Android.Webkit.WebView view, string url) {
OnPageLoaded?.Invoke(this, true);
}
}
public Browser(CustomWebViewClient wc, string url = "") {
WB = new Android.Webkit.WebView(Android.App.Application.Context);
WB.ClearCache(true);
WB.Settings.JavaScriptEnabled = true;
WB.Settings.CacheMode = CacheModes.NoCache;
WB.Settings.DomStorageEnabled = true;
WB.Settings.SetAppCacheEnabled(false);
WB.Settings.UserAgentString = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10";
WB.LoadUrl(url);
WB.SetWebViewClient(wc);
}
public async Task<string> EvalJS(string js, bool returnNullObjectWhenNull = true) {
string JSResult = "";
ManualResetEvent reset = new ManualResetEvent(false);
Device.BeginInvokeOnMainThread(() => {
WB?.EvaluateJavascript($"javascript:(function() {{ return {js}; }})()", new JSInterface((r) => {
JSResult = r;
reset.Set();
}));
});
await Task.Run(() => { reset.WaitOne(); });
return JSResult == "null" ? returnNullObjectWhenNull ? null : "null" : JSResult;
}
class JSInterface : Java.Lang.Object, IValueCallback {
private Action<string> _callback;
public JSInterface(Action<string> callback) {
_callback = callback;
}
public void OnReceiveValue(Java.Lang.Object value) {
string v = value.ToString();
if (v.StartsWith('"') && v.EndsWith('"'))
v = v.Remove(0, 1).Remove(v.Length - 2, 1);
_callback?.Invoke(v);
}
}
}
Example:
Browser.CustomWebViewClient wc = new Browser.CustomWebViewClient();
wc.OnPageLoaded += BrowserOnPageLoad;
Browser browser = new Browser(wc, "https://www.google.com/");
void BrowserOnPageLoad(object sender, bool e) {
string test = browser.EvalJS("document.getElementsByClassName('Q8LRLc')[0].innerText");
// 'test' will contain the value returned from the JS script
// You can acces the real WebView object by using
// browser.WB
}
// OR WITH THE NEW RETURNING FUNCTION
async void BrowserOnPageLoad(object sender, bool e) {
string test = await browser.EvalJS("document.getElementsByClassName('Q8LRLc')[0].innerText");
// 'test' will contain the value returned from the JS script
// You can acces the real WebView object by using
// browser.WB
}
[FINAL-SOLUTION]
Finally I've found an easy and efficient alternative to WebView.
Now I'm using SimpleBroswer and works great!
[SEMI-SOLUTION]
Alright, I've written a workaround but I don't really like this idea, so please, if you know a better method let me know.
Below my workaround:
In my ForegroundServiceHelper interface I've added a method to check if the MainActivity (where the WebView it's rendered) is visible or not, if isn't visible the MainActivity will be shown and immediately will be hidden back.
And the app will be removed from the last used applications
Method inside my ForegroundServiceHelper Interface
public void InitBackgroundWebView() {
if ((bool)SharedSettings.Entries.Get("MainPage.IsVisible") == false) {
// Shows the activity
Intent serviceIntent = new Intent(context, typeof(MainActivity));
serviceIntent.AddFlags(ActivityFlags.NewTask);
context.StartActivity(serviceIntent);
// And immediately hides it back
Intent main = new Intent(Intent.ActionMain);
main.AddFlags(ActivityFlags.NewTask);
main.AddCategory(Intent.CategoryHome);
context.StartActivity(main);
// Removes from the last app used
ActivityManager am = (new ContextWrapper(Android.App.Application.Context)).GetSystemService(Context.ActivityService).JavaCast<ActivityManager>();
if (am != null) {
System.Collections.Generic.IList<ActivityManager.AppTask> tasks = am.AppTasks;
if (tasks != null && tasks.Count > 0) {
tasks[0].SetExcludeFromRecents(true);
}
}
}
}
The SharedSettings class is an helper class wrapped around the App.Current.Properties Dictionary
And in the OnAppearing and OnDisappearing callbacks I set the shared values to true/false
[EDIT]
This workaround works only if the user is on the homepage, so I need to find an another solution...
I need to migrate some data in an Orchard module running within a Orchard 1.9.0 installation. The issue here is that the data is stored in a foreign DB on another server, not in the Orchard DB. So when the migration class methods get called, internally Orchard uses the Orchard.Data.ISessionLocator interface to retrieve the DB connection. Sadly overriding this behavior is not possible but i had the idea to hook into the session locator thing by creating a custom session locator.
The custom session locator looks like this and is based on the existing class Orchard.Data.SessionLocator:
public class CustomSessionLocator : Orchard.Data.ISessionLocator, Orchard.Data.ITransactionManager, System.IDisposable
{
// public
public CustomSessionLocator(Orchard.Data.ISessionFactoryHolder aSessionFactoryHolder)
{
Logger = Orchard.Logging.NullLogger.Instance;
IsolationLevel = System.Data.IsolationLevel.ReadCommitted;
mSessionFactoryHolder = aSessionFactoryHolder;
mSessions = new System.Collections.Generic.Dictionary<SessionScope, NHibernate.ISession>();
if (mForeignDBConnection == null)
{
string lConnectionString = "data source=myServer;initial catalog=myDB;persist security info=True;user id=xxx;password=xxx;MultipleActiveResultSets=True;";
mForeignDBConnection = new System.Data.SqlClient.SqlConnection(lConnectionString);
}
}
public NHibernate.ISession For(System.Type aEntityType)
{
Logger.Debug("Acquiring session for {0}", aEntityType);
Demand();
return mSessions[CurrentSessionScope];
}
public void Demand()
{
EnsureSession(IsolationLevel);
}
public void RequireNew()
{
RequireNew(IsolationLevel);
}
public void RequireNew(System.Data.IsolationLevel aLevel)
{
DisposeSession();
EnsureSession(aLevel);
}
public void Cancel()
{
NHibernate.ISession lSession;
if (mSessions.TryGetValue(CurrentSessionScope, out lSession) && lSession != null && !lSession.Transaction.WasRolledBack && lSession.Transaction.IsActive)
{
Logger.Debug("Rolling back transaction");
lSession.Transaction.Rollback();
}
}
public void Dispose()
{
DisposeSession();
}
public enum SessionScope
{
OrchardDefault,
ForeignDB
}
public Orchard.Logging.ILogger Logger { get; set; }
public System.Data.IsolationLevel IsolationLevel { get; set; }
public SessionScope CurrentSessionScope { private get; set; }
// private
private void DisposeSession()
{
NHibernate.ISession lSession;
if (mSessions.TryGetValue(CurrentSessionScope, out lSession) && lSession != null)
{
try
{
if (!lSession.Transaction.WasRolledBack && lSession.Transaction.IsActive)
{
Logger.Debug("Committing transaction");
lSession.Transaction.Commit();
}
}
finally
{
Logger.Debug("Disposing session");
var lConnection = lSession.Connection;
lSession.Close();
lSession.Dispose();
lSession = null;
mSessions[CurrentSessionScope] = null;
}
}
}
private void EnsureSession(System.Data.IsolationLevel aLevel)
{
NHibernate.ISession lSession;
if (mSessions.TryGetValue(CurrentSessionScope, out lSession) && lSession != null)
return;
var lSessionFactory = mSessionFactoryHolder.GetSessionFactory();
Logger.Debug("Opening NHibernate session");
if (CurrentSessionScope == SessionScope.ForeignDB)
{
lSession = lSessionFactory.OpenSession(mForeignDBConnection);
// open connection otherwise the following lSession.BeginTransaction() fails with exception
if (mForeignDBConnection.State == System.Data.ConnectionState.Closed)
mForeignDBConnection.Open();
}
else
lSession = lSessionFactory.OpenSession();
mSessions[CurrentSessionScope] = lSession;
lSession.BeginTransaction(aLevel);
}
private readonly Orchard.Data.ISessionFactoryHolder mSessionFactoryHolder;
private System.Collections.Generic.Dictionary<SessionScope, NHibernate.ISession> mSessions;
private static System.Data.SqlClient.SqlConnection mForeignDBConnection;
}
Then i have a migration data interpreter that looks like this:
public class ForeignDataMigrationInterpreter : Orchard.Data.Migration.Interpreters.DefaultDataMigrationInterpreter
{
public ForeignDataMigrationInterpreter(
Orchard.Environment.Configuration.ShellSettings aShellSettings,
Orchard.Data.ISessionLocator aSessionLocator,
System.Collections.Generic.IEnumerable<Orchard.Data.Migration.Interpreters.ICommandInterpreter> aCommandInterpreters,
Orchard.Data.ISessionFactoryHolder aSessionFactoryHolder,
Orchard.Reports.Services.IReportsCoordinator aReportsCoordinator)
: base(aShellSettings, aSessionLocator, aCommandInterpreters, aSessionFactoryHolder, aReportsCoordinator)
{
mSessionLocator = aSessionLocator as CustomSessionLocator;
}
public override void Visit(Orchard.Data.Migration.Schema.CreateTableCommand aCommand)
{
#if LIVE
if (IsForeignDBCommand(aCommand.Name, ""))
mSessionLocator.CurrentSessionScope = CustomSessionLocator.SessionScope.ForeignDB;
else
mSessionLocator.CurrentSessionScope = CustomSessionLocator.SessionScope.OrchardDefault;
#endif
base.Visit(aCommand);
}
...
private bool IsForeignDBCommand(...)
{
return ...;
}
private CustomSessionLocator mSessionLocator;
}
As you can see, the basic procedure with foreign data is
Start Orchard
Migration class method is called which contains SchemaBuilder.CreateTable()
ForeignDataMigrationInterpreter.Visit(CreateTableCommand) is called
CurrentSessionScope of custom session locator is updated to SessionScope.ForeignDB
CreateTableCommand is passed into base class
CustomSessionLocator.For() is called which
ends in CustomSessionLocator.EnsureSession() which
returns the session X for scope SessionScope.ForeignDB
base class enlists CreateTableCommand to transaction of session X
fast forward some unrelated additional steps and the transaction is commited BUT it never returns and a timeout exception occures
My questions are
Is it even possible to migrate foreign data this way?
Why does timeout occur?
Based on this page we've created a Wizard that has three steps. Everything works great, but we have one problem with the code given in the link, which is how it creates the next step instance (copy pasted from the link):
protected override IScreen DetermineNextItemToActivate(IList<IScreen> list, int lastIndex)
{
var theScreenThatJustClosed = list[lastIndex] as BaseViewModel;
var state = theScreenThatJustClosed.WorkflowState;
var nextScreenType = TransitionMap.GetNextScreenType(theScreenThatJustClosed);
var nextScreen = Activator.CreateInstance(nextScreenType, state);
return nextScreen as IScreen;
}
Currently, it looks like this in our project:
protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int lastIndex)
{
var theScreenThatJustClosed = list[lastIndex];
if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");
if (theScreenThatJustClosed.NextTransition == WizardTransition.Done)
{
TryClose(); // Close the entire Wizard
}
var state = theScreenThatJustClosed.WizardAggregateState;
var nextScreenType = _map.GetNextScreenType(theScreenThatJustClosed);
if (nextScreenType == null) return null;
// TODO: CreateInstance requires all constructors for each WizardStep, even if they aren't needed. This should be different!
var nextScreen = Activator.CreateInstance(nextScreenType, state, _applicationService, _wfdRegisterInstellingLookUp,
_adresService, _userService, _documentStore, _windowManager, _fileStore, _fileUploadService, _dialogService,
_eventAggregator, _aanstellingViewModelFactory);
return nextScreen as IWizardScreen;
}
As you can see, we have quite a few parameters we need in some steps. In step 1 we only need like two, but because of the Activator.CreateInstance(nextScreenType, state, ...); we still need to pass all of them.
What I'd like instead is to use a delegate Factory. We use them at more places in our project, and let AutoFac take care of the rest of the parameters. For each of the three steps we only need a delegate Factory that uses the state.
Because all three uses the same delegate Factory with just state, I've placed this Factory in their Base class:
public delegate WizardBaseViewModel<TViewModel> Factory(AggregateState state);
How I'd like to change the DetermineNextItemToActivate method:
protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int lastIndex)
{
var theScreenThatJustClosed = list[lastIndex];
if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");
if (theScreenThatJustClosed.NextTransition == WizardTransition.Done)
{
TryClose(); // Close the entire Wizard
}
return _map.GetNextScreenFactoryInstance(state);
}
But now I'm stuck at making the GetNextScreenFactoryInstance method:
public IWizardScreen GetNextScreenFactoryInstance(IWizardScreen screenThatClosed)
{
var state = screenThatClosed.WizardAggregateState;
// This is where I'm stuck. How do I get the instance using the Factory, when I only know the previous ViewModel
// ** Half-Pseudocode
var nextType = GetNextScreenType(screenThatClosed);
var viewModelFactory = get delegate factory based on type?;
var invokedInstance = viewModelFactory.Invoke(state);
// **
return invokedInstance as IWizardScreen;
}
Feel free to change the GetNextScreenFactoryInstance any way you'd like. As long as I can get the next Step-ViewModel based on the previous one in the map.
NOTE: Other relevant code, can be found in the link, but I'll post it here as well to keep it all together:
The WizardTransitionMap (only change is it not being a Singleton anymore, so we can instantiate a map outselves):
public class WizardTransitionMap : Dictionary<Type, Dictionary<WizardTransition, Type>>
{
public void Add<TIdentity, TResponse>(WizardTransition transition)
where TIdentity : IScreen
where TResponse : IScreen
{
if (!ContainsKey(typeof(TIdentity)))
{
Add(typeof(TIdentity), new Dictionary<WizardTransition, Type> { { transition, typeof(TResponse) } });
}
else
{
this[typeof(TIdentity)].Add(transition, typeof(TResponse));
}
}
public Type GetNextScreenType(IWizardScreen screenThatClosed)
{
var identity = screenThatClosed.GetType();
var transition = screenThatClosed.NextTransition;
if (!transition.HasValue) return null;
if (!ContainsKey(identity))
{
throw new InvalidOperationException(String.Format("There are no states transitions defined for state {0}", identity));
}
if (!this[identity].ContainsKey(transition.Value))
{
throw new InvalidOperationException(String.Format("There is no response setup for transition {0} from screen {1}", transition, identity));
}
return this[identity][transition.Value];
}
}
Our InitializeMap-method:
protected override void InitializeMap()
{
_map = new WizardTransitionMap();
_map.Add<ScreenOneViewModel, ScreenTwoViewModel>(WizardTransition.Next);
_map.Add<ScreenTwoViewModel, ScreenOneViewModel>(WizardTransition.Previous);
_map.Add<ScreenTwoViewModel, ScreenThreeViewModel>(WizardTransition.Next);
_map.Add<ScreenThreeViewModel, ScreenTwoViewModel>(WizardTransition.Previous);
_map.Add<ScreenThreeViewModel, ScreenThreeViewModel>(WizardTransition.Done);
}
We've changed the code:
The WizardTransitionMap now accepts Delegates. Also, instead of retrieving the type by the WizardTransition-enum value (Next, Previous, etc.), we now retrieve the Factory-invoke based on the next Type (so the inner Dictionary is reversed). So, this is our current WizardTransitionMap:
using System;
using System.Collections.Generic;
namespace NatWa.MidOffice.CustomControls.Wizard
{
public class WizardTransitionMap : Dictionary<Type, Dictionary<Type, Delegate>>
{
public void Add<TCurrentScreenType, TNextScreenType>(Delegate delegateFactory)
{
if (!ContainsKey(typeof(TCurrentScreenType)))
{
Add(typeof(TCurrentScreenType), new Dictionary<Type, Delegate> { { typeof(TNextScreenType), delegateFactory } });
}
else
{
this[typeof(TCurrentScreenType)].Add(typeof(TNextScreenType), delegateFactory);
}
}
public IWizardScreen GetNextScreen(IWizardScreen screenThatClosed)
{
var identity = screenThatClosed.GetType();
var state = screenThatClosed.State;
var transition = screenThatClosed.NextScreenType;
if (!ContainsKey(identity))
{
throw new InvalidOperationException(String.Format("There are no states transitions defined for state {0}", identity));
}
if (!this[identity].ContainsKey(transition))
{
throw new InvalidOperationException(String.Format("There is no response setup for transition {0} from screen {1}", transition, identity));
}
if (this[identity][transition] == null)
return null;
return (IWizardScreen)this[identity][transition].DynamicInvoke(state);
}
}
}
Our InitializeMap is now changed to this:
protected override void InitializeMap()
{
_map = new WizardTransitionMap();
_map.Add<ScreenOneViewModel, ScreenTwoViewModel>(_screenTwoFactory);
_map.Add<ScreenTwoViewModel, ScreenOneViewModel>(_screenOneFactory);
_map.Add<ScreenTwoViewModel, ScreenThreeViewModel>(_screenThreeFactory);
_map.Add<ScreenThreeViewModel, ScreenTwoViewModel>(_screenTwoFactory);
_map.Add<ScreenThreeViewModel, ScreenThreeViewModel>(null);
}
And our DetemineNexttemToActivate method to this:
protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int previousIndex)
{
var theScreenThatJustClosed = list[previousIndex];
if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");
var nextScreen = _map.GetNextScreen(theScreenThatJustClosed);
if (nextScreen == null)
{
TryClose();
return ActiveItem; // Can't return null here, because Caliburn's Conductor will automatically get into this method again with a retry
}
return nextScreen;
}
We also removed our entire WizardBaseViewModel and just let every Step-ViewModel implement the IWizardScreen:
public interface IWizardScreen : IScreen
{
AggregateState State { get; }
Type NextScreenType { get; }
void Next();
void Previous();
}
With the following implementation in our ScreenOneViewModel:
public AggregateState State { get { return _state; } }
public Type NextScreenType { get; private set; }
public void Next()
{
if (!IsValid()) return;
NextScreenType = typeof(ScreenTwoViewModel);
TryClose();
}
public void Previous()
{
throw new NotImplementedException(); // Isn't needed in first screen, because we have no previous
}
And the following implementation in our ScreenThreeViewModel:
public AggregateState State { get { return _state; } }
public Type NextScreenType { get; private set; }
public void Next()
{
NextScreenType = typeof(ScreenThreeViewModel); // Own type, because we have no next
TryClose();
}
public void Previous()
{
NextScreenType = typeof(ScreenTwoViewModel);
TryClose();
}
And each Step-ViewModel has its own delegate Factory, like this one for ScreenTwoViewModel:
public delegate ScreenTwoViewModel Factory(AggregateState state);
This is a basically a class library project which is somehow exposed as a WCF service. The code below is a part of the Data Access Layer. 'db' is an object of a DataContext class. To save a file, we do the following-
public static Guid SaveFile(FileDetails fileDetails)
{
System.Nullable<Guid> id = null;
SystemDataContext.UsingWrite(db =>
{
db.SaveFileData(fileDetails.RunId, fileDetails.FileData, fileDetails.FileExtension, ref id);
});
return id ?? Guid.Empty;
}
Then, the below would execute-
public static void UsingWrite(Action<SoftCashCreditDBDataContext> action)
{
using (var context = new SystemDataContext())
{
try
{
action(context.Write);
}
catch (Exception ex)
{
DataAccessExceptionHandler.HandleExcetion(ex, Config.DataLayerPolicy);
}
}
}
public SystemDataContext()
{
if (_stack == null)
{
_stack = new Stack<SystemDataContext>();
this.Depth = 1;
this.Read = new SoftCashCreditDBDataContext(Config.ReadDatabaseConnection);
this.Write = new SoftCashCreditDBDataContext(Config.WriteDatabaseConnection);
}
else
{
var parent = _stack.Peek();
/// Increment level of node.
this.Depth = parent.Depth + 1;
/// Copy data context from the parent
this.Read = parent.Read;
this.Write = parent.Write;
}
_stack.Push(this);
}
public int Depth { get; private set; }
public bool IsRoot { get { return this.Depth == 1; } }
[ThreadStatic]
private static Stack<SystemDataContext> _stack = null;
public SoftCashCreditDBDataContext Read { get; private set; }
public SoftCashCreditDBDataContext Write { get; private set; }
#region IDisposable Members
public void Dispose()
{
var context = _stack.Pop();
if (context.IsRoot == true)
{
context.Read.Dispose();
context.Write.Dispose();
_stack = null;
}
}
#endregion
}
They have implemented LINQ to SQL here, and created a DBContext class. The 'SaveFileData()' method is actually part of that class, where it just calls an SP inside to save the file.
What I did not follow-
What exactly does the call to UsingWrite() do here? What is passed to the 'Action action' parameter, and what is it doing?
I understand your confusion. They use 2 delegates.
This is passed to the action parameter:
db =>
{
db.SaveFileData(fileDetails.RunId, fileDetails.FileData, fileDetails.FileExtension, ref id);
}
So when UsingWrite is called, the SoftCashCreditDBDataContext delegate which was set in the Write delegate will call SaveFileData.
A simplified example to help you understand Action:
public void Main()
{
Test(x => Debug.Write(x));
}
private void Test(Action<string> testAction)
{
testAction("Bla");
}
This function will call Debug.Write with the argument x, which is a string that is passed to the test action function.