CefSharp ignores Preferences set with RequestContext (Spellcheck) - c#

I try to enable spell check for the CefSharp Chromium embedded browser (v3.3396.1786 installed with NuGet) and the CefSharp.WPF component (v67). I can get spell check to work with a single language but I'm not able to change the dictionary for spellchecking at runtime. I tried the examples shown and linked on CefSharps github page but without success.
My CefSharp browser always uses the the Locale to determine the language to use for spell checking no matter what I set with RequestContext.SetPreference()
This is my code which initializes Cef:
public static void Initialize()
{
var settings = new CefSettings
{
BrowserSubprocessPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
Environment.Is64BitProcess ? "x64" : "x86",
"CefSharp.BrowserSubprocess.exe"),
Locale = "de-DE",
RemoteDebuggingPort = 8088,
};
// Set BrowserSubProcessPath based on app bitness at runtime
// Make sure you set performDependencyCheck false
Cef.Initialize
(
settings,
performDependencyCheck: false,
browserProcessHandler: null
);
CefSharpSettings.LegacyJavascriptBindingEnabled = true;
}
The actual browser is set up and created in another method:
private void create_web_browser()
{
current_web_browser = new ChromiumWebBrowser
{
Visibility = Visibility.Hidden,
BrowserSettings = new BrowserSettings
{
FileAccessFromFileUrls = CefState.Enabled,
UniversalAccessFromFileUrls = CefState.Enabled,
Javascript = CefState.Enabled,
ImageLoading = CefState.Enabled,
JavascriptAccessClipboard = CefState.Enabled,
JavascriptCloseWindows = CefState.Enabled,
JavascriptDomPaste = CefState.Enabled
}
};
current_helper = new ChromiumObjectForScriptingHelper(web_browser_ready_async, current_web_browser);
if (ToolbarConfig != null)
{
current_helper.SetToolbarConfig(ToolbarConfig);
}
current_web_browser.RegisterJsObject("callbackObj", current_helper);
var cur_dir = Directory.GetCurrentDirectory();
var url = $"file://{cur_dir}/ckeditor/editor.html";
current_web_browser.Address = url;
current_web_browser.RequestContext = new RequestContext();
current_web_browser.RequestContext.SetPreference("browser.enable_spellchecking", true, out _);
current_web_browser.RequestContext.SetPreference("spellcheck.dictionaries", new List<string> { "en-US" }, out _);
grid.Children.Add(current_web_browser);
}
An additional method is used to enable the user to change language later:
public void SetSpellcheck(Spellcheck language)
{
if (language == Spellcheck.None) return;
current_web_browser.RequestContext.SetPreference("spellcheck.dictionaries", new List<string> { get_locale_for_language(language) }, out _);
}
As you can see I try to set the spell checking settings but no matter what I set there, it has no effect. I could set enable_spellcheckto false and it still checks the spelling and the dictionaries I set are also ignored. Instead of what I put in dictionaries, the language previously set in Locale will be used. (I checked the out variable but there were no errors)
I also tried using the global RequestContext but with no success.
Apparently other people got it to work somehow so I'm feeling like I miss something important here, or doing something completely stupid.
Another thing is that, if I use GetAllPreferences(true), to get a list of all the settings with defaults, I just get null.

Thanks to #amaitlands's comments I now know that the issue was that I was setting the preferences in the wrong thread. I had the misconception that CefSharp was running in my applications UI-Thread, when it actually was running in its own.
The solution is to use Cef.UIThreadTaskFactory.StartNew() to run the code inside of the CefSharp UI-Thread
Cef.UIThreadTaskFactory.StartNew(delegate
{
current_web_browser.RequestContext.SetPreference("browser.enable_spellchecking", true, out _);
current_web_browser.RequestContext.SetPreference("spellcheck.dictionaries", new List<object> { "en-US" }, out _);
});
I also had to change the type of the List<> to object since I'm using an older version of CefSharp.WPF otherwise I'd get a Trying to set a preference of type LIST to value of type NULL error.

Related

UIDocumentPickerViewController.ModalPresentationStyle can't be changed in iOS 16.1

I've got a app that depends on Xamarin.IOS.
It at some point can display a document picker as a popover.
After updating to XCode14 and doing a build for iOS16 I found the document picker was displaying incorrectly (In its FormSheet style rather than in the Popover style).
The reason for this seems to be that attempting to change ModalPresentationStyle is silently failing and remaining as the same default value - FormSheet.
Reproduced it outside of the app in a simple test app with a single button click handler.
Here I'd expect the ModalPresentationStyle to change or at least throw some sort of an error if not supported. Instead, it silently remains as UIModalPresentationStyle.FormSheet.
partial void BtnClick(UIKit.UIButton sender)
{
var allowedUtis = new List<string>() { ".txt" };
var documentPicker = new UIDocumentPickerViewController(
allowedUtis.ToArray(),
UIDocumentPickerMode.Import);
var previousValue = documentPicker.ModalPresentationStyle;
documentPicker.ModalPresentationStyle = UIModalPresentationStyle.Popover;
Debug.WriteLine($"Changed from {previousValue} to {documentPicker.ModalPresentationStyle}");
if (documentPicker.PopoverPresentationController != null)
{
documentPicker.PopoverPresentationController.SourceView = sender;
documentPicker.PopoverPresentationController.SourceRect = sender.Bounds;
documentPicker.PopoverPresentationController.PermittedArrowDirections = UIPopoverArrowDirection.Up;
}
PresentModalViewController(documentPicker, true);
}
Also reproduced this same behaviour in a test app in swift to check it wasn't Xamarin.IOS that was the problem. Again here the value of modalPresentationStyle remains as .formSheet (2).
let supportedTypes: [UTType] = [UTType.audio]
let pickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true) 1017
print(String(describing: pickerViewController.modalPresentationStyle));
pickerViewController.modalPresentationStyle = .popover
print(String(describing: pickerViewController.modalPresentationStyle));
self.present(pickerViewController, animated: true, completion: {})
This didn't happen on XCode13 but does on XCode14.01 on an 8th gen iPad running iOS 16.1.
Can't be reproduced on XCode14.01 with a simulator running iOS 16.0.
Has the expected behavior changed? I can't seem to find anything in the release notes of documentation about this.
I ran into the same basic issue recently myself. The solution is to setup the popoverPresentationController after you present the modal view controller.
let supportedTypes: [UTType] = [UTType.audio]
let pickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes, asCopy: true) 1017
print(String(describing: pickerViewController.modalPresentationStyle));
pickerViewController.modalPresentationStyle = .popover
print(String(describing: pickerViewController.modalPresentationStyle));
self.present(pickerViewController, animated: true)
pickerViewController.popoverPresentationController?.sourceRect = sender.bounds
pickerViewController.popoverPresentationController?.sourceView = sender
pickerViewController.popoverPresentationController?.permittedArrowDirections = .up
The view controller's popoverPresentationController is nil before the call to present. At least that is the case under iOS 16.
While I didn't test this specifically with UIDocumentPickerViewController, this solution worked for a general modal view controller I was trying to present. The view controller would appear as a form sheet, despite being set as a popover, if the popoverPresentationController was setup before the call to present instead of after.

How to mirror AdaptiveChoiceSetInput behavior on mobile and desktop devices?

Working on a bot for teams using the Bot Framework SDK, I noticed that the AdaptiveChoiceSetInput (dropdowns) in cards don't behave the same way on mobile and desktop devices.
On desktop Teams, if a dropdown does not have a value, it defaults to a placeholder Select. This automatic defaulting also allows for validation to force a choice to be selected from the dropdown.
On mobile Teams, if a dropdown does not have a value, it instead defaults to the first choice. This is obviously incorrect as it makes it appear like a choice is selected in the dropdown, when it's really null.
A solution I tried was to manually add a default choice with value of null so that it would automatically set itself on mobile if the value was null. This caused an issue where the card did not appear on a mobile device.
Another solution was to add a value, like 0. Although it's likely possible to make things work this way, it lead to some complicated code I never finished because on desktop I had to account for the placeholder and the manually added default choice, and figure out how to prevent the form to be submitted with the manually added default choice.
How can I make the AdaptiveChoiceSetInput behave the same way on mobile version of Teams as in the desktop version?
Here's the relevant code:
private async Task<Attachment> GetEditableOrder(OrderModel order)
{
List<AdaptiveChoice> orderAdaptiveChoices = _context.GetOrderAdaptiveChoices(order.id);
var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0));
card.Body.Add(new AdaptiveColumnSet
{
Columns = new List<AdaptiveColumn>
{
new AdaptiveColumn
{
Items = new List<AdaptiveElement>
{
new AdaptiveTextBlock
{
Text = "**Order**"
}
}
},
new AdaptiveColumn
{
Items = new List<AdaptiveElement>
{
new AdaptiveChoiceSetInput
{
Id = "orderId",
Choices = orderAdaptiveChoices,
Value = order.Id.ToString(),
Style = AdaptiveChoiceInputStyle.Compact,
Placeholder = "Select",
IsRequired = true,
ErrorMessage = "Selection required."
}
}
}
}
});
card.Actions.Add(new AdaptiveSubmitAction
{
Type = AdaptiveSubmitAction.TypeName,
Title = "Submit",
Data = new JObject {
{ "submitLocation", "editOrder" }
},
});
Attachment attachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = card
};
return attachment;
}
I solved my issue by manually adding an additional default choice titled "Select" with the value of an empty string. The values null and 0 did not work due to the issues I mentioned in my question. But an empty string seems to behave the same way as the placeholder in desktop version of Teams.
So, to make the dropdown in the cards behave the same way on desktop and mobile version of Teams, simply prepend a default AdaptiveChoice to your list with the value of an empty string:
orderAdaptiveChoices.Insert(0, new AdaptiveChoice { Value = "", Title = "Select" });
Whenever it is selected on either device, it will function the same way as the placeholder that is available only on desktop Teams.

Selenium can't handle multiple ChromiumWebBrowser instances in C#

I have two instances of the ChromiumWebBrowser in my WinForms project (Visual Studio 2012). My goal is to have the second browser instance "copy" the behavior of the user input in the first browser instance. I can successfully retrieve the input from the first browser, and I managed to hook up Selenium in the project as well.
However, I'm having one issue. Whenever Selenium sends its commands, the first browser is the one that responds to them. For the life of me, I can't seem to figure out how to make the second browser respond. Whenever I completely remove the first browser, the second one starts responding correctly, but adding the first browser again will make only have the first browser use the Selenium commands. I even tried to switch out the moments the browsers are added to the form, but to no avail: whenever there are two available, the wrong one is responsive.
Relevant code:
public BrowserManager(Controller controller, string startingUrl)
{
_controller = controller;
var settings = new CefSettings { RemoteDebuggingPort = 9515 };
Cef.Initialize(settings);
// Input browser
inputBrowser = new ChromiumWebBrowser(startingUrl);
var obj = new XPathHelper(this);
inputBrowser.RegisterJsObject("bound", obj); //Standard object registration
inputBrowser.FrameLoadEnd += obj.OnFrameLoadEnd;
// Output browser
var browserSettings = new BrowserSettings();
var requestContextSettings = new RequestContextSettings { CachePath = "" };
var requestContext = new RequestContext(requestContextSettings);
outputBrowser = new ChromiumWebBrowser(startingUrl);
outputBrowser.RequestContext = requestContext;
outputBrowser.AddressChanged += InitializeOutputBrowser;
outputBrowser.Enabled = false;
outputBrowser.Name = "outputBrowser";
}
The selenium part:
public class SeleniumHelper
{
public SeleniumHelper()
{
DoWorkAsync();
}
private Task DoWorkAsync()
{
Task.Run(() =>
{
string chromeDriverDir = #"ActionRecorder\bin\x64\Debug\Drivers";
var chromeDriverService = ChromeDriverService.CreateDefaultService(chromeDriverDir);
chromeDriverService.HideCommandPromptWindow = true;
ChromeOptions options = new ChromeOptions();
options.BinaryLocation = #"ActionRecorder\bin\x64\Debug\ActionRecorder.exe";
options.DebuggerAddress = "127.0.0.1:9515";
options.AddArguments("--enable-logging");
using (IWebDriver driver = new OpenQA.Selenium.Chrome.ChromeDriver(chromeDriverService, options))
{
driver.Navigate().GoToUrl("http://www.google.com");
var query = driver.FindElement(By.Name("q"));
query.SendKeys("A google search test");
query.Submit();
}
});
return null;
}
}
And finally, a screenshot for some visualization:
Some help with the issue would be very much appreciated. If i missed some crucial info, feel free to ask for it. Thanks in advance!
Greetz,
Tybs
The behavior is correct. You have one debug address and you can only have one debug address for CEF. Which means when you use Selenium it is only seeing one browser.
By default Selenium will send an command to current active Tab or Window. Now in your case you have multiple Chrome view embedded, but they are technically Chrome Tab/Windows which you have placed on the same form.
So if you are in luck below code in should be able to move you to the Window you are interested in
driver.SwitchTo().Window(driver.WindowHandles.Last());
See if it works. If it doesn't then your only other workaround would be to change the order of Adding ChromiumWebBrowser and that should reverse the window it works on.
Below are some important threads that you should read from top to bottom. Very relevant to your issue/request
https://code.google.com/archive/p/chromiumembedded/issues/421
https://github.com/cefsharp/CefSharp/issues/1076

Is it possible to set/edit a file extended properties with Windows API Code Pack?

I'd like to know if it's possible to set/edit a file extended properties (Explorer: Right-click > Properties > Details) using the Windows API Code Pack.
var shellFile = Microsoft.WindowsAPICodePack.Shell.ShellObject.FromParsingName(filePath);
var artistName = shellFile.Properties.GetProperty(SystemProperties.System.Music.DisplayArtist).ValueAsObject.ToString();
var duration = TimeSpan.FromMilliseconds(Convert.ToDouble(shellFile.Properties.GetProperty(SystemProperties.System.Media.Duration).ValueAsObject) * 0.0001);
I use these few lines to get the properties I want, but I don't know how to edit one of them (the artist name for example).
I know I can use taglib-sharp, but I'll use it only if there is no solution without external code.
Thanks you all for taking the time to help me.
I found a way to edit some properties with ShellPropertyWriter but some properties are read-only.
var shellFile = ShellFile.FromParsingName(filePath);
ShellPropertyWriter w = shellFile.Properties.GetPropertyWriter();
try
{
w.WriteProperty(SystemProperties.System.Author, new string[] { "MyTest", "Test" });
w.WriteProperty(SystemProperties.System.Music.Artist, new string[] { "MyTest", "Test" });
w.WriteProperty(SystemProperties.System.Music.DisplayArtist, "Test");
}
catch (Exception ex)
{
}
w.Close();
In this sample, the 2 first occurences of ShellPropertyWriter.WriteProperty() will do exactly the same, edit the "Contributing artists" field of the file (Explorer: Right-click > Properties > Details). The third call will throw an "Access denied" exception.
Some are editable, others are not. Just need to try.
You can write to the ShellFile directly by setting the value of the properties without ShellPropertyWriter:
var shellFile = ShellFile.FromFilePath(filePath);
shellFile.Properties.System.Author.Value = new string[] { "MyTest", "Test" };
shellFile.Properties.System.Music.Artist.Value = new string[] { "MyTest", "Test" };
shellFile.Properties.System.Music.DisplayArtist.Value = "Test";
Just be aware, that to be able to edit codec-specific fields of a file, it's necessary to have the codec installed on the computer.

How to access WinRM in C#

I'd like to create a small application that can collect system information (Win32_blablabla) using WinRM as opposed to WMI. How can i do that from C#?
The main goal is to use WS-Man (WinRm) as opposed to DCOM (WMI).
I guess the easiest way would be to use WSMAN automation. Reference wsmauto.dll from windwos\system32 in your project:
then, code below should work for you. API description is here: msdn: WinRM C++ API
IWSMan wsman = new WSManClass();
IWSManConnectionOptions options = (IWSManConnectionOptions)wsman.CreateConnectionOptions();
if (options != null)
{
try
{
// options.UserName = ???;
// options.Password = ???;
IWSManSession session = (IWSManSession)wsman.CreateSession("http://<your_server_name>/wsman", 0, options);
if (session != null)
{
try
{
// retrieve the Win32_Service xml representation
var reply = session.Get("http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Service?Name=winmgmt", 0);
// parse xml and dump service name and description
var doc = new XmlDocument();
doc.LoadXml(reply);
foreach (var elementName in new string[] { "p:Caption", "p:Description" })
{
var node = doc.GetElementsByTagName(elementName)[0];
if (node != null) Console.WriteLine(node.InnerText);
}
}
finally
{
Marshal.ReleaseComObject(session);
}
}
}
finally
{
Marshal.ReleaseComObject(options);
}
}
hope this helps, regards
I've got an article that describes an easy way to run Powershell through WinRM from .NET at http://getthinktank.com/2015/06/22/naos-winrm-windows-remote-management-through-net/.
The code is in a single file if you want to just copy it and it's also a NuGet package that includes the reference to System.Management.Automation.
It auto manages trusted hosts, can run script blocks, and also send files (which isn't really supported but I created a work around). The returns are always the raw objects from Powershell.
// this is the entrypoint to interact with the system (interfaced for testing).
var machineManager = new MachineManager(
"10.0.0.1",
"Administrator",
MachineManager.ConvertStringToSecureString("xxx"),
true);
// will perform a user initiated reboot.
machineManager.Reboot();
// can run random script blocks WITH parameters.
var fileObjects = machineManager.RunScript(
"{ param($path) ls $path }",
new[] { #"C:\PathToList" });
// can transfer files to the remote server (over WinRM's protocol!).
var localFilePath = #"D:\Temp\BigFileLocal.nupkg";
var fileBytes = File.ReadAllBytes(localFilePath);
var remoteFilePath = #"D:\Temp\BigFileRemote.nupkg";
machineManager.SendFile(remoteFilePath, fileBytes);
Hope this helps, I've been using this for a while with my automated deployments. Please leave comments if you find issues.
I would like to note that this shows an interop error by default in Visual Studio 2010.
c.f. http://blogs.msdn.com/b/mshneer/archive/2009/12/07/interop-type-xxx-cannot-be-embedded-use-the-applicable-interface-instead.aspx
There appear to be two ways to solve this. This first is documented in the article listed above and appears to be the correct way to handle the problem. The pertinent changes for this example is:
WSMan wsManObject = new WSMan();
This is in lieu of IWSMan wsman = new WSManClass(); which will throw the error.
The second resolution is to go to the VS2010—>Solution Explorer—>Solution—>Project—>References and select WSManAutomation. Right click or hit Alt-Enter to access the properties. Change the value of the "Embed Interop Types" property of the wsmauto reference.

Categories