Task cancellation: while loop not exited properly when ThrowIfCancellationRequested() is called - wpf

I've developed a small project (using MVVM) with functionality to upload a file to a FTP-server.
The user can view the uploading progress: percentage completed is shown to the user by vm.Busycontent, which is a property in my viewmodel bound to a UI element in my view.
Here is the code for reading the file and uploading via FTP (which is part of the Task vm.FtpUploadTask)
using (FileStream inputStream = File.OpenRead(file))
{
using (outputStream = request.GetRequestStream())
{
var buffer = new byte[1024 * 1024];
int totalReadBytesCount = 0;
int readBytesCount;
while ((readBytesCount = inputStream.Read(buffer, 0, buffer.Length)) > 0 && (!vm.Token.IsCancellationRequested))
{
vm.Token.ThrowIfCancellationRequested();
outputStream.Write(buffer, 0, readBytesCount);
totalReadBytesCount += readBytesCount;
var progress = totalReadBytesCount * 100.0 / inputStream.Length;
vm.BusyContent = ((int)progress).ToString();
}
}
}
MainWindow.xaml
I am using WPF extended toolkit BusyIndicator
<xctk:BusyIndicator IsBusy='{Binding IsBusy}'>
<xctk:BusyIndicator.BusyContentTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text='{Binding Path=DataContext.BusyContent,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}' />
</StackPanel>
</DataTemplate>
</xctk:BusyIndicator.BusyContentTemplate>
</xctk:BusyIndicator>
UploadToFTPCommand.cs
try
{
vm.FtpUploadTask = new Task(() => FTPUpload(file), vm.Token);
vm.FtpUploadTask.Start();
vm.FtpUploadTask.Wait(vm.Token);
vm.BusyContent = "upload done!";
}
catch (OperationCanceledException)
{
vm.BusyContent = "Canceled";
}
CancelCommand.cs
public class CancelCommand : ICommand
{
public void Execute(object parameter)
{
vm.TokenSource.Cancel();
}
}
The cancel function works but sometimes vm.Busycontent equals to
((int)progress).ToString()
canceled in uploadftpcommand
When pressing the Cancel button, the while loop should be exited and the user should only see the message in the catch (OperationCanceledException).
Any ideas how to solve this?
Notes
I am using .NET 4.0
This program is a part of a larger project, which includes multipe Tasks that should be executed in a synchronous manner. That's why I am using Task.Start() and Task.Wait() methods.
Edit
Problem still remains
using (FileStream inputStream = File.OpenRead(file))
{
using (outputStream = request.GetRequestStream())
{
var buffer = new byte[1024 * 1024];
int totalReadBytesCount = 0;
int readBytesCount;
while ((readBytesCount = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
if (vm.Token.IsCancellationRequested)
{
break;
}
outputStream.Write(buffer, 0, readBytesCount);
totalReadBytesCount += readBytesCount;
var progress = totalReadBytesCount * 100.0 / inputStream.Length;
vm.BusyContent = ((int)progress).ToString();
}
if (vm.Token.IsCancellationRequested)
{
inputStream.Close();
outputStream.Close();
vm.Token.ThrowIfCancellationRequested();
}
}
}

You may have an issue relating to exactly how you've set up your code. Without going through every line of it, it's difficult to tell. But more to the point:
When pressing the Cancel button, the while loop should be exited and the user should only see the message in the catch (OperationCanceledException).
This shows exactly how to exit your while loop and throw an exception you can catch. Note the test asserts TaskCanceledException but you are right in just catching OperationCanceledException.
using System.Threading.Tasks;
using System.IO;
using System.Threading;
using NUnit.Framework;
namespace CancellationTests {
[TestFixture]
public class WhileCancellation {
[Test]
public void While_Loop_Is_Canceled_When_Cancel_Is_Requested() {
var inputFile = new FileInfo("somebigfile.txt");
var outputFile = new FileInfo("outputFile.txt");
var cts = new CancellationTokenSource();
cts.Cancel();
Assert.ThrowsAsync<TaskCanceledException>(() => TheWhileLoop(inputFile, outputFile, cts.Token));
}
private async Task TheWhileLoop(FileInfo inputFile, FileInfo outputFile, CancellationToken token) {
using (var inputStream = inputFile.OpenRead())
using (var outputStream = outputFile.OpenWrite()) {
var buffer = new byte[1024 * 1024];
var totalReadBytesCount = 0;
var readBytesCount = 0;
while ((readBytesCount = await inputStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) {
await outputStream.WriteAsync(buffer, 0, readBytesCount, token);
token.ThrowIfCancellationRequested();
totalReadBytesCount += readBytesCount;
var progress = totalReadBytesCount * 100.0 / inputStream.Length;
vm.BusyContent = ((int)progress).ToString();
}
}
}
private static class vm {
public static string BusyContent { get; set; }
}
}
}
Hopefully this gets you on the right track.

Related

How can I "stream" search results from a minimal API using IAsyncEnumerable in c# using .NET 6?

Overview
We're moving our database access to API calls and I need to return search results from a database as they're being found using that API. The plan was to use IAsyncEnumerable, but I'm having trouble dealing with the HttpClient buffer (at least I think). The call is being triggered by a button click on the client side in WPF that sends the search term to a client side API service. That service makes the API request which then uses a repository to do the work. I'm using a minimal API and both the client and API are using .Net 6.
Problem
The problem I'm having is that, while the IAsyncEnumerable part works on the API side by yield returning each entry one at a time, the entire result still gets passed back as a single response instead of yielding back one response at a time.
Info & Questions
At this point I've simplified it down just to try and get the concept to work. The IAsyncEnumerable part works just fine within the client ApiService itself when it's not using HttpClient, but as soon as that is introduced I start running into issues. You can see the various solutions I've tried in the Regions of the GetTestStreamAsync method.
So I guess one of the main questions is, can IAsyncEnumerable even do this? And if not, what's the solution?
Client-side Code
MainWindow.xaml.cs
private async void btnSearch_Click(object sender, RoutedEventArgs e)
{
// This works
//var result = apiService.GetTestAsync();
// This doesn't
var result = apiService.GetTestStreamAsync(txtSearchTerms.Text);
await foreach(var item in result!)
{
lstResults.Items.Add(item);
}
}
MainWindow.xaml
<Grid>
<TextBox x:Name="txtSearchTerms"
HorizontalAlignment="Left"
Margin="86,92,0,0"
TextWrapping="Wrap"
VerticalAlignment="Top"
Width="340"
TabIndex="1" />
<Button x:Name="btnSearch"
Content="Search"
HorizontalAlignment="Left"
Margin="387,115,0,0"
VerticalAlignment="Top"
TabIndex="2"
Click="btnSearch_Click" />
<ListBox x:Name="lstResults"
HorizontalAlignment="Left"
Margin="86,140,0,0"
VerticalAlignment="Top"
Height="204"
Width="340"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
Padding="10,10,10,10"
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Grid>
ApiServics.cs
public class ApiService : IApiService
{
// This works
public async IAsyncEnumerable<int> GetTestAsync()
{
for (int i = 1; i < 6; i++)
{
yield return i;
await Task.Delay(250);
}
}
// This doesn't
public async IAsyncEnumerable<string> GetTestStreamAsync(string input)
{
HttpClient httpClient = new() { BaseAddress = new Uri($"https://localhost:7203/test/{input}") };
#region nope 6 (but seems closer as it hits the await foreach and lists individually in xaml listbox, but still waits for all results)
Stream? response = await httpClient.GetStreamAsync(httpClient.BaseAddress);
await foreach (string? item in System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable<string>(response))
{
yield return item!;
}
#endregion
#region nope 8 (from https://github.com/dotnet/aspnetcore/issues/12883)
//using Stream? stream = await httpClient.GetStreamAsync(httpClient.BaseAddress);
//using StreamReader? reader = new(stream);
//using JsonTextReader? jsonReader = new(reader);
//Newtonsoft.Json.JsonSerializer serializer = new();
//while (await jsonReader.ReadAsync())
//{
// var result = serializer.Deserialize<object>(jsonReader);
// yield return result?.ToString()!;
//}
#endregion
#region nope 7 (from https://stackoverflow.com/questions/69992725/streaming-lines-of-text-using-iasyncenumerable)
//using var request = new HttpRequestMessage(HttpMethod.Get, httpClient.BaseAddress);
//request.SetBrowserResponseStreamingEnabled(true);
//var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
//response.EnsureSuccessStatusCode();
//using var responseStream = await response.Content.ReadAsStreamAsync();
//using var reader = new StreamReader(responseStream);
//string? line = null;
//while ((line = await reader.ReadLineAsync()) != null)
//{
// yield return line;
//}
#endregion
#region nope 5
//using Stream response = await httpClient.GetStreamAsync(httpClient.BaseAddress);
//using StreamReader reader = new StreamReader(response);
//while (!reader.EndOfStream)
//{
// string? line = await reader.ReadLineAsync();
// yield return line!;
//}
#endregion
#region nope 4
//HttpResponseMessage response = await httpClient.GetAsync(httpClient.BaseAddress);
//response.EnsureSuccessStatusCode();
//var stream = await response.Content.ReadAsStreamAsync();
//await foreach (string? item in JsonSerializer.DeserializeAsyncEnumerable<string>(stream))
//{
// yield return item!;
//}
#endregion
#region nope 3
//HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, httpClient.BaseAddress);
//request.SetBrowserResponseStreamingEnabled(true);
//using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
//response.EnsureSuccessStatusCode();
//using Stream responseStream = await response.Content.ReadAsStreamAsync();
//await foreach (string? item in JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream))
//{
// yield return item!;
//}
#endregion
#region nope 2
//var response = await httpClient.GetStringAsync(httpClient.BaseAddress);
//var responseStr = JsonSerializer.Deserialize<string>(response);
//yield return responseStr!;
#endregion
#region nope 1
//HttpResponseMessage response = await httpClient.GetAsync(httpClient.BaseAddress);
//response.EnsureSuccessStatusCode();
//yield return await response.Content.ReadAsStringAsync();
#endregion
}
}
IApiService.cs
public interface IApiService
{
public IAsyncEnumerable<int> GetTestAsync();
public IAsyncEnumerable<string> GetTestStreamAsync(string input);
}
API Code
Program.cs
app.MapGet("/test/{stringInput}", IAsyncEnumerable<string> (string stringInput, IDataRepository rep) =>
{
return TestStreamAsync();
async IAsyncEnumerable<string> TestStreamAsync()
{
IAsyncEnumerable<string> results = rep.TestStreamAsync(stringInput);
await foreach (string item in results)
{
yield return item;
}
}
}).WithName("TestStream");
DataRepository.cs
internal class DataRepository : IDataRepository
{
public async IAsyncEnumerable<string> TestStreamAsync(string input)
{
for (int i = 1; i < 6; i++)
{
yield return $"{input}-{i}";
await Task.Delay(250);
}
}
}
IDataRepository.cs
internal interface IDataRepository
{
IAsyncEnumerable<string> TestStreamAsync(string input);
}

Use BitmapDecoder in an other thread then where is created. (The calling thread cannot access this object because a different thread owns it.)

So, i have a function that will load an image from disk async in an other thread ( big images will be loaded and I don't want the UI THread to be locked while loading).
Loading is done like this
public override void LoadFile()
{
using (var imageStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Decoder = new TiffBitmapDecoder(imageStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
InitializeFile();
}
}
Then I want to use the Decoder on the main thread
public List<ThumbnailModel> LoadPages()
{
var result = new List<ThumbnailModel>();
foreach (var frame in Decoder.Frames) <--// this line throws exception
{
result.Add(new ThumbnailModel
{
Name = _metadataLoader.GetPageName((BitmapMetadata)frame.Metadata),
Bitmap = new WriteableBitmap(frame)
});
}
return result;
}
Now here is the problem, whenever I reach the line where I try to access the Decoder.Frames it throws exception (The calling thread cannot access this object because a different thread owns it.)
Is there a way I can use my Decoder in the main thread if not, the only possible solution is to load all the image information in the other thread?
Full code version :
// this is the task, that calls the imageFactory LoadFile method - NewThread
private async Task OpenFileAsync(string strFilePath)
{
var newFile = _imageFileFactory.LoadFile(strFilePath);
if (newFile != null)
{
_imagefile = newFile;
}
}
//image factory load file - NewThread
public IImageFile LoadFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
return null;
}
var fileExtension = Path.GetExtension(filePath); // .tiff or .jpeg
var file = new ImageFileTiff(filePath, _metatadaFactory, _metadataVersioner);
file.LoadFile();
return file;
}
// ImageFileTiff LoadFile will create a decoder - NewThread
public override void LoadFile()
{
using (var imageStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Decoder = new JpegBitmapDecoder(imageStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
InitializeFile();
}
}
After we have an IImageFile we call on MainThread(UIThread)
var pages = _imagefile.LoadPages();
Where LoadPages is the place where the app breaks. also called on UIThread
public List LoadPages()
{
var result = new List();
foreach (var frame in Decoder.Frames)
{
result.Add(new ThumbnailModel
{
Name = _metadataLoader.GetPageName((BitmapMetadata)frame.Metadata),
Bitmap = new WriteableBitmap(frame)
});
}
return result;
}
I thought you could simply return the decoder from the thread to be able to access it but your decoder is a TiffBitmapDecoder which inherits from DispatcherObject (https://learn.microsoft.com/en-gb/dotnet/api/system.windows.threading.dispatcherobject?view=netcore-3.1).
So you won't be able to access it from a different thread than the one where it was created msdn:"Only the thread that the Dispatcher was created on may access the DispatcherObject directly"
What you could do instead is use the decoder in it's thread and return the final result:
I couldn't build on your sample since there was to much missing for me to test it but I built a similar project to give an exemple:
public partial class MainWindow : Window
{
public MainWindow()
{
}
public TiffBitmapDecoder LoadFile()
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.InitialDirectory = "c:\\";
openFileDialog.Filter = "tiff files (*.tif)|*.tif|All files (*.*)|*.*";
openFileDialog.FilterIndex = 2;
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == true && !string.IsNullOrEmpty(openFileDialog.FileName))
{
//I didn't bother to check the file extension since it's just an exemple
using (var imageStream = openFileDialog.OpenFile())
{
return new TiffBitmapDecoder(imageStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
}
}
else
{
//User cancelled
return null;
}
}
public List<ThumbnailModel> LoadPages(TiffBitmapDecoder decoder)
{
//TiffBitmapDecoder" inherits from DispatcherObject/>
//https://learn.microsoft.com/en-gb/dotnet/api/system.windows.threading.dispatcherobject?view=netcore-3.1
var result = new List<ThumbnailModel>();
if (decoder != null)
{
try
{
foreach (var frame in decoder.Frames)
{
result.Add(new ThumbnailModel
{
//set the variables
});
}
}
catch(InvalidOperationException e)
{
MessageBox.Show(e.Message, "Error");
}
}
else
{
//Nothing to do
}
return result;
}
private async Task AsyncLoading()
{
this.thumbnailModels = await Task.Run<List<ThumbnailModel>>(() =>
{
var decoder = this.LoadFile();
return this.LoadPages(decoder);
});
}
private List<ThumbnailModel> thumbnailModels = null;
private async void AsyncLoadingButton_Click(object sender, RoutedEventArgs e)
{
await this.AsyncLoading();
}
}
public class ThumbnailModel
{
}
Content of MainWindow.xaml just in case:
<Grid>
<StackPanel Orientation="Vertical">
<Button x:Name="NoReturnButton" Margin="10" HorizontalAlignment="Center" Content="Call AsyncLoadingNoReturn" Click="AsyncLoadingButton_Click" />
</StackPanel>
</Grid>

How to call wpf function via JS when visiting https link

I need to execute a function defined in wpf project, which is called from JS in a https web page.
The demo project of all codes is here: https://github.com/tomxue/WebViewIssueInWpf
JS part:
The web page link is https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=
And it contains below line:
<script src="js/index.js" type="text/javascript" charset="utf-8"></script>
And js/index.js contains below code:
setTitle(dataObject.city + weekDay(dataObject.date) +"天气" )
setTitle() is defined below: uses method of window.external.notify()
function setTitle(_str){
try{
wtjs.setTitle(_str)
}catch(e){
console.log(_str)
window.external.notify(_str);
}
}
The function window.external.notify() will call wpf function via ScriptNotify().
WPF part:
For the WebView inside of the wpf project
this.wv.IsScriptNotifyAllowed = true;
this.wv.ScriptNotify += Wv_ScriptNotify;
And
private void Wv_ScriptNotify(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlScriptNotifyEventArgs e)
{
textBlock.Text = e.Value;
}
Problems:
(1)
The problem here is if the web page uses https://, then the above function Wv_ScriptNotify() in wpf will not be fired. But if the web page link uses http://, then the above function Wv_ScriptNotify() in wpf can be fired.
Why and how to solve it?
Update:
2020-3-2 17:25:55, tested just now, https works. I do not know what causes https does not work previously
(2)
JS in the web page uses a object wtjs (defined by ourselves and work well with an UWP project using JSBridge).
And I want to use a similiar method to UWP, using a bridge so that I can add multiple funtions/interfaces for JS to call. The disadvantage of ScriptNotify() is that only one interface is usable.
To achieve it, I make below code, which is commented out now.
wv.RegisterName("wtjs", new myBridge());
And more functions are defined as below
public class myBridge
{
public void SetTitle(string title)
{
Debug.WriteLine("SetTitle is executing...title = {0}", title);
}
public void PlayTTS(string tts)
{
Debug.WriteLine("PlayTTS is executing...tts = {0}", tts);
}
}
While in JS side, corresponding functions will be called.
wtjs.playTTS(tts)
wtjs.setTitle(_str)
But in fact wpf side did not work, while the UWP project using JSBridge works with the web link(so web page and JS script are workable). How to achieve it?
(3)
The above two problems are solved by DK Dhilip's answer already.
But a new problem is found. Please check my GitHub code, update it to latest commit.
https://github.com/tomxue/WebViewIssueInWpf
I put a TextBlock onto WebView and expect to see the text floating on the web content. But in fact, the text is covered by the WebView. Why and how to solve it?
Thanks!
For Problem (1, 2)
HTTPS link worked fine for me, maybe the page is too slow to load?
According to Microsoft (source), only ScriptNotify is supported in WebView:
Can I inject native objects into my WebViewControl content?
No.
Neither the WebBrower (Internet Explorer) ObjectForScripting property
nor the WebView (UWP) AddWebAllowedObject method are supported in
WebViewControl. As a workaround, you can use window.external.notify/
ScriptNotify and JavaScript execution to communicate between the
layers, for example:
https://github.com/rjmurillo/WebView_AddAllowedWebObjectWorkaround
But the above suggested workaround solution seems to work differently to your expectation, so I just implement my own solution to emulate the JSBridge convention you have expected.
My custom solution is not battle-tested, it might break in some edge cases but it seems to work fine in few simple tests.
What's supported:
Multiple bridge objects
JS to C# method call
JS to C# get/set property
C# Usage:
// Add
webView.AddWebAllowedObject("wtjs", new MyBridge(this));
webView.AddWebAllowedObject("myBridge", new MyOtherBridge());
// Remove
webView.RemoveWebAllowedObject("wtjs");
JS Usage:
// Call C# object method (no return value)
wtjs.hello('hello', 'world', 666);
myBridge.saySomething('天猫精灵,叫爸爸!');
// Call C# object method (return value)
wtjs.add(10, 20).then(function (result) { console.log(result); });
// Get C# object property
wtjs.backgroundColor.then(function (color) { console.log(color); });
// Set C# object property
wtjs.niubility = true;
Code
WebViewExtensions.cs
using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using Microsoft.Toolkit.Wpf.UI.Controls;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Text;
namespace WpfApp3
{
// Source: https://github.com/dotnet/orleans/issues/1269#issuecomment-171233788
public static class JsonHelper
{
private static readonly Type[] _specialNumericTypes = { typeof(ulong), typeof(uint), typeof(ushort), typeof(sbyte) };
public static object ConvertWeaklyTypedValue(object value, Type targetType)
{
if (targetType == null)
throw new ArgumentNullException(nameof(targetType));
if (value == null)
return null;
if (targetType.IsInstanceOfType(value))
return value;
var paramType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (paramType.IsEnum)
{
if (value is string)
return Enum.Parse(paramType, (string)value);
else
return Enum.ToObject(paramType, value);
}
if (paramType == typeof(Guid))
{
return Guid.Parse((string)value);
}
if (_specialNumericTypes.Contains(paramType))
{
if (value is BigInteger)
return (ulong)(BigInteger)value;
else
return Convert.ChangeType(value, paramType);
}
if (value is long || value is double)
{
return Convert.ChangeType(value, paramType);
}
return value;
}
}
public enum WebViewInteropType
{
Notify = 0,
InvokeMethod = 1,
InvokeMethodWithReturn = 2,
GetProperty = 3,
SetProperty = 4
}
public class WebAllowedObject
{
public WebAllowedObject(WebView webview, string name)
{
WebView = webview;
Name = name;
}
public WebView WebView { get; private set; }
public string Name { get; private set; }
public ConcurrentDictionary<(string, WebViewInteropType), object> FeaturesMap { get; } = new ConcurrentDictionary<(string, WebViewInteropType), object>();
public EventHandler<WebViewControlNavigationCompletedEventArgs> NavigationCompletedHandler { get; set; }
public EventHandler<WebViewControlScriptNotifyEventArgs> ScriptNotifyHandler { get; set; }
}
public static class WebViewExtensions
{
public static bool IsNotification(this WebViewControlScriptNotifyEventArgs e)
{
try
{
var message = JsonConvert.DeserializeObject<dynamic>(e.Value);
if (message["___magic___"] != null)
{
return false;
}
}
catch (Exception) { }
return true;
}
public static void AddWebAllowedObject(this WebView webview, string name, object targetObject)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name));
if (targetObject == null)
throw new ArgumentNullException(nameof(targetObject));
if (webview.Tag == null)
{
webview.Tag = new ConcurrentDictionary<string, WebAllowedObject>();
}
else if (!(webview.Tag is ConcurrentDictionary<string, WebAllowedObject>))
{
throw new InvalidOperationException("WebView.Tag property is already being used for other purpose.");
}
var webAllowedObjectsMap = webview.Tag as ConcurrentDictionary<string, WebAllowedObject>;
var webAllowedObject = new WebAllowedObject(webview, name);
if (webAllowedObjectsMap.TryAdd(name, webAllowedObject))
{
var objectType = targetObject.GetType();
var methods = objectType.GetMethods();
var properties = objectType.GetProperties();
var jsStringBuilder = new StringBuilder();
jsStringBuilder.Append("(function () {");
jsStringBuilder.Append("window['");
jsStringBuilder.Append(name);
jsStringBuilder.Append("'] = {");
jsStringBuilder.Append("__callback: {},");
jsStringBuilder.Append("__newUuid: function () { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function (c) { return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16); }); },");
foreach (var method in methods)
{
if (!method.IsSpecialName)
{
if (method.ReturnType == typeof(void))
{
webAllowedObject.FeaturesMap.TryAdd((method.Name, WebViewInteropType.InvokeMethod), method);
}
else
{
webAllowedObject.FeaturesMap.TryAdd((method.Name, WebViewInteropType.InvokeMethodWithReturn), method);
}
var parameters = method.GetParameters();
var parametersInString = string.Join(",", parameters.Select(x => x.Position).Select(x => "$$" + x.ToString()));
jsStringBuilder.Append(method.Name);
jsStringBuilder.Append(": function (");
jsStringBuilder.Append(parametersInString);
jsStringBuilder.Append(") {");
if (method.ReturnType != typeof(void))
{
jsStringBuilder.Append("var callbackId = window['" + name + "'].__newUuid();");
}
jsStringBuilder.Append("window.external.notify(JSON.stringify({");
jsStringBuilder.Append("source: '");
jsStringBuilder.Append(name);
jsStringBuilder.Append("',");
jsStringBuilder.Append("target: '");
jsStringBuilder.Append(method.Name);
jsStringBuilder.Append("',");
jsStringBuilder.Append("parameters: [");
jsStringBuilder.Append(parametersInString);
jsStringBuilder.Append("]");
if (method.ReturnType != typeof(void))
{
jsStringBuilder.Append(",");
jsStringBuilder.Append("callbackId: callbackId");
}
jsStringBuilder.Append("}), ");
jsStringBuilder.Append((method.ReturnType == typeof(void)) ? (int)WebViewInteropType.InvokeMethod : (int)WebViewInteropType.InvokeMethodWithReturn);
jsStringBuilder.Append(");");
if (method.ReturnType != typeof(void))
{
jsStringBuilder.Append("var promise = new Promise(function (resolve, reject) {");
jsStringBuilder.Append("window['" + name + "'].__callback[callbackId] = { resolve, reject };");
jsStringBuilder.Append("});");
jsStringBuilder.Append("return promise;");
}
jsStringBuilder.Append("},");
}
}
jsStringBuilder.Append("};");
foreach (var property in properties)
{
jsStringBuilder.Append("Object.defineProperty(");
jsStringBuilder.Append("window['");
jsStringBuilder.Append(name);
jsStringBuilder.Append("'], '");
jsStringBuilder.Append(property.Name);
jsStringBuilder.Append("', {");
if (property.CanRead)
{
webAllowedObject.FeaturesMap.TryAdd((property.Name, WebViewInteropType.GetProperty), property);
jsStringBuilder.Append("get: function () {");
jsStringBuilder.Append("var callbackId = window['" + name + "'].__newUuid();");
jsStringBuilder.Append("window.external.notify(JSON.stringify({");
jsStringBuilder.Append("source: '");
jsStringBuilder.Append(name);
jsStringBuilder.Append("',");
jsStringBuilder.Append("target: '");
jsStringBuilder.Append(property.Name);
jsStringBuilder.Append("',");
jsStringBuilder.Append("callbackId: callbackId,");
jsStringBuilder.Append("parameters: []");
jsStringBuilder.Append("}), ");
jsStringBuilder.Append((int)WebViewInteropType.GetProperty);
jsStringBuilder.Append(");");
jsStringBuilder.Append("var promise = new Promise(function (resolve, reject) {");
jsStringBuilder.Append("window['" + name + "'].__callback[callbackId] = { resolve, reject };");
jsStringBuilder.Append("});");
jsStringBuilder.Append("return promise;");
jsStringBuilder.Append("},");
}
if (property.CanWrite)
{
webAllowedObject.FeaturesMap.TryAdd((property.Name, WebViewInteropType.SetProperty), property);
jsStringBuilder.Append("set: function ($$v) {");
jsStringBuilder.Append("window.external.notify(JSON.stringify({");
jsStringBuilder.Append("source: '");
jsStringBuilder.Append(name);
jsStringBuilder.Append("',");
jsStringBuilder.Append("target: '");
jsStringBuilder.Append(property.Name);
jsStringBuilder.Append("',");
jsStringBuilder.Append("parameters: [$$v]");
jsStringBuilder.Append("}), ");
jsStringBuilder.Append((int)WebViewInteropType.SetProperty);
jsStringBuilder.Append(");");
jsStringBuilder.Append("},");
}
jsStringBuilder.Append("});");
}
jsStringBuilder.Append("})();");
var jsString = jsStringBuilder.ToString();
webAllowedObject.NavigationCompletedHandler = (sender, e) =>
{
var isExternalObjectCustomized = webview.InvokeScript("eval", new string[] { "window.external.hasOwnProperty('isCustomized').toString();" }).Equals("true");
if (!isExternalObjectCustomized)
{
webview.InvokeScript("eval", new string[] { #"
(function () {
var originalExternal = window.external;
var customExternal = {
notify: function (message, type = 0) {
if (type === 0) {
originalExternal.notify(message);
} else {
originalExternal.notify(JSON.stringify({
___magic___: true,
type: type,
interop: message
}));
}
},
isCustomized: true
};
window.external = customExternal;
})();" });
}
webview.InvokeScript("eval", new string[] { jsString });
};
webAllowedObject.ScriptNotifyHandler = (sender, e) =>
{
try
{
var message = JsonConvert.DeserializeObject<dynamic>(e.Value);
if (message["___magic___"] != null)
{
var interopType = (WebViewInteropType)message.type;
var interop = JsonConvert.DeserializeObject<dynamic>(message.interop.ToString());
var source = (string)interop.source.ToString();
var target = (string)interop.target.ToString();
var parameters = (object[])interop.parameters.ToObject<object[]>();
if (interopType == WebViewInteropType.InvokeMethod)
{
if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
{
if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object methodObject))
{
var method = (MethodInfo)methodObject;
var parameterTypes = method.GetParameters().Select(x => x.ParameterType).ToArray();
var convertedParameters = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
convertedParameters[i] = JsonHelper.ConvertWeaklyTypedValue(parameters[i], parameterTypes[i]);
}
method.Invoke(targetObject, convertedParameters);
}
}
}
else if (interopType == WebViewInteropType.InvokeMethodWithReturn)
{
var callbackId = interop.callbackId.ToString();
if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
{
if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object methodObject))
{
var method = (MethodInfo)methodObject;
var parameterTypes = method.GetParameters().Select(x => x.ParameterType).ToArray();
var convertedParameters = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
convertedParameters[i] = JsonHelper.ConvertWeaklyTypedValue(parameters[i], parameterTypes[i]);
}
var invokeResult = method.Invoke(targetObject, convertedParameters);
webview.InvokeScript("eval", new string[] { string.Format("window['{0}'].__callback['{1}'].resolve({2}); delete window['{0}'].__callback['{1}'];", source, callbackId, JsonConvert.SerializeObject(invokeResult)) });
}
}
}
else if (interopType == WebViewInteropType.GetProperty)
{
var callbackId = interop.callbackId.ToString();
if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
{
if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object propertyObject))
{
var property = (PropertyInfo)propertyObject;
var getResult = property.GetValue(targetObject);
webview.InvokeScript("eval", new string[] { string.Format("window['{0}'].__callback['{1}'].resolve({2}); delete window['{0}'].__callback['{1}'];", source, callbackId, JsonConvert.SerializeObject(getResult)) });
}
}
}
else if (interopType == WebViewInteropType.SetProperty)
{
if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
{
if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object propertyObject))
{
var property = (PropertyInfo)propertyObject;
property.SetValue(targetObject, JsonHelper.ConvertWeaklyTypedValue(parameters[0], property.PropertyType));
}
}
}
}
}
catch (Exception ex)
{
// Do nothing
}
};
webview.NavigationCompleted += webAllowedObject.NavigationCompletedHandler;
webview.ScriptNotify += webAllowedObject.ScriptNotifyHandler;
}
else
{
throw new InvalidOperationException("Object with the identical name is already exist.");
}
}
public static void RemoveWebAllowedObject(this WebView webview, string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name));
var allowedWebObjectsMap = webview.Tag as ConcurrentDictionary<string, WebAllowedObject>;
if (allowedWebObjectsMap != null)
{
if (allowedWebObjectsMap.TryRemove(name, out WebAllowedObject webAllowedObject))
{
webview.NavigationCompleted -= webAllowedObject.NavigationCompletedHandler;
webview.ScriptNotify -= webAllowedObject.ScriptNotifyHandler;
webview.InvokeScript("eval", new string[] { "delete window['" + name + "'];" });
}
}
}
}
}
MainWindow.xaml.cs
using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using System;
using System.Diagnostics;
using System.Windows;
namespace WpfApp3
{
public partial class MainWindow : Window
{
public class MyBridge
{
private readonly MainWindow _window;
public MyBridge(MainWindow window)
{
_window = window;
}
public void setTitle(string title)
{
Debug.WriteLine(string.Format("SetTitle is executing...title = {0}", title));
_window.setTitle(title);
}
public void playTTS(string tts)
{
Debug.WriteLine(string.Format("PlayTTS is executing...tts = {0}", tts));
}
}
public MainWindow()
{
this.InitializeComponent();
this.wv.IsScriptNotifyAllowed = true;
this.wv.ScriptNotify += Wv_ScriptNotify;
this.wv.AddWebAllowedObject("wtjs", new MyBridge(this));
this.Loaded += MainPage_Loaded;
}
private void Wv_ScriptNotify(object sender, WebViewControlScriptNotifyEventArgs e)
{
if (e.IsNotification())
{
Debug.WriteLine(e.Value);
}
}
private void setTitle(string str)
{
textBlock.Text = str;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
this.wv.Source = new Uri("https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=");
}
}
}
Result
Screenshot:
For Problem (3)
According to (1, 2, 3), it is impossible to overlay UI elements on top of WebView/WebBrowser control.
Luckily there is an alternative solution called CefSharp which is based on Chromium web browser and would be good enough for your use case, plus the background animation worked (which doesn't work in original WebView control).
However, there is no perfect solution; WPF design view is unusable with CefSharp (showing Invalid Markup error), but the program will just compile and run. Also, the project can only be built with either x86 or x64 option, AnyCPU will not work.
MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
x:Class="WpfApp3.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid x:Name="grid">
<cefSharp:ChromiumWebBrowser x:Name="wv" HorizontalAlignment="Left" Height="405" Margin="50,0,0,0" VerticalAlignment="Top" Width="725" RenderTransformOrigin="-0.45,-0.75" />
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="30,30,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="60" Width="335"/>
</Grid>
</Window>
MainWindow.xaml.cs
using CefSharp;
using System.Diagnostics;
using System.Windows;
namespace WpfApp3
{
public partial class MainWindow : Window
{
public class MyBridge
{
private readonly MainWindow _window;
public MyBridge(MainWindow window)
{
_window = window;
}
public void setTitle(string title)
{
Debug.WriteLine(string.Format("SetTitle is executing...title = {0}", title));
_window.setTitle(title);
}
public void playTTS(string tts)
{
Debug.WriteLine(string.Format("PlayTTS is executing...tts = {0}", tts));
}
}
public MainWindow()
{
this.InitializeComponent();
this.wv.JavascriptObjectRepository.Register("wtjs", new MyBridge(this), true, new BindingOptions() { CamelCaseJavascriptNames = false });
this.wv.FrameLoadStart += Wv_FrameLoadStart;
this.Loaded += MainPage_Loaded;
}
private void Wv_FrameLoadStart(object sender, FrameLoadStartEventArgs e)
{
if (e.Url.StartsWith("https://cmsdev.lenovo.com.cn/musichtml/leHome/weather"))
{
e.Browser.MainFrame.ExecuteJavaScriptAsync("CefSharp.BindObjectAsync('wtjs');");
}
}
private void setTitle(string str)
{
this.Dispatcher.Invoke(() =>
{
textBlock.Text = str;
});
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
this.wv.Address = "https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=";
}
}
}
Screenshot:

In a WPF application (with Devexpress), Store data for retrieval after the application is reopened

I want to save the contents of a SelectedItem or Item's from a ComboBox as well as DataGrid Column Order so as retain the information when the application is reopened.
Initially I am using the below code for saving the data as long as the application is open:
App.Current.Properties[1] = SelectedDataSetList;
App.Current.Properties[2] = SelectedModuleList;
App.Current.Properties[0] = SelectedContentSet;
SelectedDataSetList is bound to a ComboBox:
<dxe:ComboBoxEdit Text="SCOPE" x:Name="ContentSetCombobox" Grid.Column="1" Height="25" IncrementalFiltering="True" ItemsSource="{Binding ContentSetList}" DisplayMember="Name" AllowUpdateTwoWayBoundPropertiesOnSynchronization="False" SelectedItem="{Binding SelectedContentSet,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" >
But, I was unable to store the information in a Chache memory for retrieving even if the application is closed and opened again.
Is there any way to do this without using an external file like .xml?
If you're looking to save to IsolatedStorage you can use this class I've put together (see below). It's not perfect and will fail if you try to save a type that isn't marked as Serializable but it's good enough for casual use. I've left exception handling as an exercise for the OP.
public class IsolatedStorageManager
{
public void Save<T>(T item, string key)
{
var isf = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null);
using (var writeStream = new IsolatedStorageFileStream(key, FileMode.Create, isf))
{
Serialise(item, writeStream);
}
}
public T Open<T>(string key)
{
var isf = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null);
using (var readStream = new IsolatedStorageFileStream(key, FileMode.Open, isf))
{
var item = Deserialise<T>(readStream);
return item;
}
}
private Stream Serialise<T>(T item, Stream stream)
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, item);
return stream;
}
private T Deserialise<T>(Stream stream)
{
var formatter = new BinaryFormatter();
var item = formatter.Deserialize(stream);
return (T) item;
}
}
Saving classes and datasets is demonstrated in the test fixture below.
[TestFixture]
public class IsolatedStorageManagerTestFixture
{
private IsolatedStorageManager _underTest;
private const string SaveFileKey = "TestSaveFileKey";
[SetUp]
public void SetUp()
{
_underTest = new IsolatedStorageManager();
}
[Test]
public void TestSavingDataset()
{
var tableName = "TestTable";
var ds = new DataSet();
ds.Tables.Add(new DataTable(tableName));
_underTest.Save(ds, SaveFileKey);
var saved = _underTest.Open<DataSet>(SaveFileKey);
Assert.That(saved.Tables.Count==1);
Assert.That(saved.Tables[0].TableName == tableName);
}
[Test]
public void TestSavingClass()
{
var list = new ArrayList {"Hello", new DataTable(), 2};
_underTest.Save(list,SaveFileKey);
var saved = _underTest.Open<ArrayList>(SaveFileKey);
Assert.That(saved.Count==3);
Assert.That(string.Equals((string)saved[0], "Hello"));
Assert.That(list[1] is DataTable);
Assert.That((int)list[2] == 2);
}
}

Issues with adding custom control to winforms tabcontrol

I am having trouble adding a custom zoom-able an pan-able picture box control to a tabcontrol.tabpage dynamically at runtime. I have tried a lot and was wondering if any of you smart fellas might have some advice for a poor noob like myself... here is some code...
using Microsoft.VisualBasic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace TAQTv4
{
public class ZoomPanPicBox : ScrollableControl
{
private Image _image;
//Double buffer the control
public ZoomPanPicBox()
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
this.AutoScroll = true;
this.Image = null;
this.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
this.Zoom = 1f;
}
//New
[Category("Appearance"), Description("The image to be displayed")]
public Image Image
{
get { return _image; }
set
{
_image = value;
UpdateScaleFactor();
Invalidate();
}
}
private float _zoom = 1f;
[Category("Appearance"), Description("The zoom factor. Less than 1 to reduce. More than 1 to magnify.")]
public float Zoom
{
get { return _zoom; }
set
{
if (value < 0 || value < 1E-05)
{
value = 1E-05f;
}
_zoom = value;
UpdateScaleFactor();
Invalidate();
}
}
private void UpdateScaleFactor()
{
if (_image == null)
{
this.AutoScrollMargin = this.Size;
}
else
{
this.AutoScrollMinSize = new Size(Convert.ToInt32(this._image.Width * _zoom + 0.5f), Convert.ToInt32(this._image.Height * _zoom + 0.5f));
}
}
//UpdateScaleFactor
private InterpolationMode _interpolationMode = InterpolationMode.High;
[Category("Appearance"), Description("The interpolation mode used to smooth the drawing")]
public InterpolationMode InterpolationMode
{
get { return _interpolationMode; }
set { _interpolationMode = value; }
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
}
//OnPaintBackground
protected override void OnPaint(PaintEventArgs e)
{
//if no image, don't bother. I tried check for IsNothing(_image) but this test wasn't detecting a no-image.
if (_image == null)
{
base.OnPaintBackground(e);
return;
}
//Added because the first test sometimes failed
try
{
int H = _image.Height;
//Throws an exception if image is nothing.
}
catch (Exception ex)
{
base.OnPaintBackground(e);
return;
}
//Set up a zoom matrix
Matrix mx = new Matrix(_zoom, 0, 0, _zoom, 0, 0);
mx.Translate(this.AutoScrollPosition.X / _zoom, this.AutoScrollPosition.Y / _zoom);
e.Graphics.Transform = mx;
e.Graphics.InterpolationMode = _interpolationMode;
e.Graphics.DrawImage(_image, new Rectangle(0, 0, this._image.Width, this._image.Height), 0, 0, _image.Width, _image.Height, GraphicsUnit.Pixel);
base.OnPaint(e);
}
//OnPaint
}
}
//ZoomPicBox
Now this seems to work fine while using the designer... but when trying to add images and controls at runtime the tabs instantiate fine but the zoomPicBox control does nothing so it would seem... This is how I am using it....
public void loadImagesToTabControl()
{
int i = 0;
foreach (Bitmap bitmap in intDwg.getBitmaps())
{
//ToDo add pic boxes and tabs and bitmaps to tabcontrol1
TAQTv4.ZoomPanPicBox picBox = new TAQTv4.ZoomPanPicBox();
picBox.Image = bitmap;
picBox.Anchor = AnchorStyles.Top;
picBox.Anchor = AnchorStyles.Bottom;
picBox.Anchor = AnchorStyles.Left;
picBox.Anchor = AnchorStyles.Right;
picBox.AutoScroll = true;
picBox.CausesValidation = true;
picBox.Visible = true;
picBox.Zoom = 1;
picBox.BackgroundImageLayout = ImageLayout.Tile;
picBox.Location = new System.Drawing.Point(0, 0);
picBox.TabStop = true;
picBox.Enabled = true;
picBox.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
picBox.CreateControl();
string title = "Pg " + (tabControl1.TabCount + 1).ToString();
TabPage myTabPage = new TabPage(title);
tabControl1.TabPages.Add(myTabPage);
tabControl1.TabPages[i].Controls.Add(picBox);
i++;
/* Possible pictureBox Implementation...
string title = "Pg " + (tabControl1.TabCount + 1).ToString();
TabPage myTabPage = new TabPage(title);
tabControl1.TabPages.Add(myTabPage);
PictureBox picbox = new PictureBox();
picbox.Anchor = AnchorStyles.Top;
picbox.Anchor = AnchorStyles.Bottom;
picbox.Anchor = AnchorStyles.Left;
picbox.Anchor = AnchorStyles.Right;
picbox.Image = bitmap;
picbox.Height = 800;
picbox.Width = 1300;
tabControl1.TabPages[i].Controls.Add(picbox);
i++;
*/
}
}
}
And as a last note... the pictureBox implementation worked fine as well so I know I am pulling my images from disk fine in the deserialization method of my intDwg class. Any thoughts would be much appreciated! Thanks in advance!
UPDATE:
I got the control to load pictures by setting backgroundimage to bitmap instead of picBox.Image.... FRUSTRATING .... but it seems that the way I have it set up the image is not anchored correctly ... trying to improve this and work it out now... any tips and tricks would be just awesome! Thanks!
UPDATE:
A Screen shot... as you can see the tab pages load correctly and one for each bitmap in my collection, yet the custom zoomPanPicBox control does not seem to want to display! See Bellow:
ahh .... seems I don't have rep to post pics.... ... alright how about...
https://www.dropbox.com/s/ogj5jlcce831n3p/scrst.png?v=0mcns
...
UPDATE AGAIN GOT IT THANKS All was missing setting the size as you had mentioned using the following: picBox.SetBounds(0, 0, 300, 300);
:D:D:D:D:D:D:)
Also, instead of using a counter:
TabPage myTabPage = new TabPage(title);
tabControl1.TabPages.Add(myTabPage);
tabControl1.TabPages[i].Controls.Add(picBox);
i++;
Just use your "myTabPage" reference:
TabPage myTabPage = new TabPage(title);
myTabPage.Controls.Add(picBox);
tabControl1.TabPages.Add(myTabPage);

Resources