I've got one Form, custom drawing, no controls.
When I need to get the input from the user I go to OnKeyDown event and then map event's KeyData to a string** - so that I can recognize that user has pressed "ctrl X", etc, so everything is almost ok... the problem is with different keyboard settings and language specific letters - so for today my users get
ã -> a
ó -> o
So I need the KeyPress event right? But how do I know that user typed in for example 'ł' and not alt+L? I can't get my head around that for a while now...
** - I need to do it like that since I let users to configure shortcuts and using .ToString() produces not so user friendly text like "OemPeriod, Control" or even better: "ControlKey, Control"
Yes, you'll need the KeyPress event. Or rather, override OnKeyPress. Mapping virtual key codes in the KeyDown event to key strokes is quite difficult, you'd have to be aware of the current keyboard layout. Take a look at the MSDN docs for ToUnicodeEx() to see what you are up against.
You don't have to worry about keystroke combinations like Alt+L. They don't generate a KeyPress event.
Here's an example. Start a new Windows Forms project and make the code look like this:
using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication1 {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
StringBuilder mText = new StringBuilder();
protected override void OnKeyPress(KeyPressEventArgs e) {
if (e.KeyChar == '\b') {
if (mText.Length > 0) mText.Remove(mText.Length - 1, 1);
}
else mText.Append(e.KeyChar);
Invalidate();
}
protected override void OnPaint(PaintEventArgs e) {
TextFormatFlags fmt = TextFormatFlags.Left;
TextRenderer.DrawText(e.Graphics, mText.ToString(), this.Font, this.ClientRectangle, Color.Black, fmt);
}
}
}
Related
I'm working on a virtual keyboard, but i struggle to send Keys. Nothing happens, except when i hard-add them to the textbox. Here's my code
public static void Send(Key key)
{ //Found online
var e = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, key)
{
RoutedEvent = Keyboard.KeyDownEvent
};
InputManager.Current.ProcessInput(e);
}
private void ClickOnKey(object sender, RoutedEventArgs e)
{
CustomButton cb = sender as CustomButton;
string text = cb.text;
Console.WriteLine(text);
element_to_focus.Focus(); //The textbox
Send(translate_string_to_key(text));
}
ProcessInput seems to only raise the appropriate key-related events, but will not actually modify the text.
If you really want to simulate key presses, you'll need to leverage p/invoke and use SendInput. Luckily, someone's already done the p/invoke work for us and has made their work available:
https://archive.codeplex.com/?p=inputsimulator
It's also available as a NuGet package (I found two versions):
https://www.nuget.org/packages/InputSimulator/
https://www.nuget.org/packages/InputSimulatorPlus/
I want the user to select a directory where a file that I will then generate will be saved. I know that in WPF I should use the OpenFileDialog from Win32, but unfortunately the dialog requires file(s) to be selected - it stays open if I simply click OK without choosing one. I could "hack up" the functionality by letting the user pick a file and then strip the path to figure out which directory it belongs to but that's unintuitive at best. Has anyone seen this done before?
You can use the built-in FolderBrowserDialog class for this. Don't mind that it's in the System.Windows.Forms namespace.
using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
{
System.Windows.Forms.DialogResult result = dialog.ShowDialog();
}
If you want the window to be modal over some WPF window, see the question How to use a FolderBrowserDialog from a WPF application.
EDIT: If you want something a bit more fancy than the plain, ugly Windows Forms FolderBrowserDialog, there are some alternatives that allow you to use the Vista dialog instead:
Third-party libraries, such as Ookii dialogs (.NET 4.5+)
The Windows API Code Pack-Shell:
using Microsoft.WindowsAPICodePack.Dialogs;
...
var dialog = new CommonOpenFileDialog();
dialog.IsFolderPicker = true;
CommonFileDialogResult result = dialog.ShowDialog();
Note that this dialog is not available on operating systems older than Windows Vista, so be sure to check CommonFileDialog.IsPlatformSupported first.
I created a UserControl which is used like this:
<UtilitiesWPF:FolderEntry Text="{Binding Path=LogFolder}" Description="Folder for log files"/>
The xaml source looks like this:
<UserControl x:Class="Utilities.WPF.FolderEntry"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<Button Margin="0" Padding="0" DockPanel.Dock="Right" Width="Auto" Click="BrowseFolder">...</Button>
<TextBox Height="Auto" HorizontalAlignment="Stretch" DockPanel.Dock="Right"
Text="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" />
</DockPanel>
</UserControl>
and the code-behind
public partial class FolderEntry {
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(FolderEntry), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(FolderEntry), new PropertyMetadata(null));
public string Text { get { return GetValue(TextProperty) as string; } set { SetValue(TextProperty, value); }}
public string Description { get { return GetValue(DescriptionProperty) as string; } set { SetValue(DescriptionProperty, value); } }
public FolderEntry() { InitializeComponent(); }
private void BrowseFolder(object sender, RoutedEventArgs e) {
using (FolderBrowserDialog dlg = new FolderBrowserDialog()) {
dlg.Description = Description;
dlg.SelectedPath = Text;
dlg.ShowNewFolderButton = true;
DialogResult result = dlg.ShowDialog();
if (result == System.Windows.Forms.DialogResult.OK) {
Text = dlg.SelectedPath;
BindingExpression be = GetBindingExpression(TextProperty);
if (be != null)
be.UpdateSource();
}
}
}
}
As stated in earlier answers, FolderBrowserDialog is the class to use for this. Some people have (justifiable) concerns with the appearance and behaviour of this dialog. The good news is that it was "modernized" in NET Core 3.0, so is now a viable option for those writing either Windows Forms or WPF apps targeting that version or later (you're out of luck if still using NET Framework though).
In .NET Core 3.0, Windows Forms users [sic] a newer COM-based control that was introduced in Windows Vista:
To reference System.Windows.Forms in a NET Core WPF app, it is necessary to edit the project file and add the following line:
<UseWindowsForms>true</UseWindowsForms>
This can be placed directly after the existing <UseWPF> element.
Then it's just a case of using the dialog:
using System;
using System.Windows.Forms;
...
using var dialog = new FolderBrowserDialog
{
Description = "Time to select a folder",
UseDescriptionForTitle = true,
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)
+ Path.DirectorySeparatorChar,
ShowNewFolderButton = true
};
if (dialog.ShowDialog() == DialogResult.OK)
{
...
}
FolderBrowserDialog has a RootFolder property that supposedly "sets the root folder where the browsing starts from" but whatever I set this to it didn't make any difference; SelectedPath seemed to be the better property to use for this purpose, however the trailing backslash is required.
Also, the ShowNewFolderButton property seems to be ignored as well, the button is always shown regardless.
Ookii folder dialog can be found at Nuget.
PM> Install-Package Ookii.Dialogs.Wpf
And, example code is as below.
var dialog = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog();
if (dialog.ShowDialog(this).GetValueOrDefault())
{
textBoxFolderPath.Text = dialog.SelectedPath;
}
More information on how to use it: https://github.com/augustoproiete/ookii-dialogs-wpf
For those who don't want to create a custom dialog but still prefer a 100% WPF way and don't want to use separate DDLs, additional dependencies or outdated APIs, I came up with a very simple hack using the Save As dialog.
No using directive needed, you may simply copy-paste the code below !
It should still be very user-friendly and most people will never notice.
The idea comes from the fact that we can change the title of that dialog, hide files, and work around the resulting filename quite easily.
It is a big hack for sure, but maybe it will do the job just fine for your usage...
In this example I have a textbox object to contain the resulting path, but you may remove the related lines and use a return value if you wish...
// Create a "Save As" dialog for selecting a directory (HACK)
var dialog = new Microsoft.Win32.SaveFileDialog();
dialog.InitialDirectory = textbox.Text; // Use current value for initial dir
dialog.Title = "Select a Directory"; // instead of default "Save As"
dialog.Filter = "Directory|*.this.directory"; // Prevents displaying files
dialog.FileName = "select"; // Filename will then be "select.this.directory"
if (dialog.ShowDialog() == true) {
string path = dialog.FileName;
// Remove fake filename from resulting path
path = path.Replace("\\select.this.directory", "");
path = path.Replace(".this.directory", "");
// If user has changed the filename, create the new directory
if (!System.IO.Directory.Exists(path)) {
System.IO.Directory.CreateDirectory(path);
}
// Our final value is in path
textbox.Text = path;
}
The only issues with this hack are :
Acknowledge button still says "Save" instead of something like "Select directory", but in a case like mines I "Save" the directory selection so it still works...
Input field still says "File name" instead of "Directory name", but we can say that a directory is a type of file...
There is still a "Save as type" dropdown, but its value says "Directory (*.this.directory)", and the user cannot change it for something else, works for me...
Most people won't notice these, although I would definitely prefer using an official WPF way if microsoft would get their heads out of their asses, but until they do, that's my temporary fix.
Ookii Dialogs includes a dialog for selecting a folder (instead of a file):
https://github.com/ookii-dialogs
For Directory Dialog to get the Directory Path, First Add reference System.Windows.Forms, and then Resolve, and then put this code in a button click.
var dialog = new FolderBrowserDialog();
dialog.ShowDialog();
folderpathTB.Text = dialog.SelectedPath;
(folderpathTB is name of TextBox where I wana put the folder path, OR u can assign it to a string variable too i.e.)
string folder = dialog.SelectedPath;
And if you wana get FileName/path, Simply do this on Button Click
FileDialog fileDialog = new OpenFileDialog();
fileDialog.ShowDialog();
folderpathTB.Text = fileDialog.FileName;
(folderpathTB is name of TextBox where I wana put the file path, OR u can assign it to a string variable too)
Note: For Folder Dialog, the System.Windows.Forms.dll must be added to the project, otherwise it wouldn't work.
I found the below code on below link... and it worked
Select folder dialog WPF
using Microsoft.WindowsAPICodePack.Dialogs;
var dlg = new CommonOpenFileDialog();
dlg.Title = "My Title";
dlg.IsFolderPicker = true;
dlg.InitialDirectory = currentDirectory;
dlg.AddToMostRecentlyUsedList = false;
dlg.AllowNonFileSystemItems = false;
dlg.DefaultDirectory = currentDirectory;
dlg.EnsureFileExists = true;
dlg.EnsurePathExists = true;
dlg.EnsureReadOnly = false;
dlg.EnsureValidNames = true;
dlg.Multiselect = false;
dlg.ShowPlacesList = true;
if (dlg.ShowDialog() == CommonFileDialogResult.Ok)
{
var folder = dlg.FileName;
// Do something with selected folder string
}
I'd suggest, to add in the nugget package:
Install-Package OpenDialog
Then the way to used it is:
Gat.Controls.OpenDialogView openDialog = new Gat.Controls.OpenDialogView();
Gat.Controls.OpenDialogViewModel vm = (Gat.Controls.OpenDialogViewModel)openDialog.DataContext;
vm.IsDirectoryChooser = true;
vm.Show();
WPFLabel.Text = vm.SelectedFilePath.ToString();
Here's the documentation:
http://opendialog.codeplex.com/documentation
Works for Files, files with filter, folders, etc
The best way to achieve what you want is to create your own wpf based control , or use a one that was made by other people
why ? because there will be a noticeable performance impact when using the winforms dialog in a wpf application (for some reason)
i recommend this project
https://opendialog.codeplex.com/
or Nuget :
PM> Install-Package OpenDialog
it's very MVVM friendly and it isn't wraping the winforms dialog
The Ookii VistaFolderBrowserDialog is the one you want.
If you only want the Folder Browser from Ooki Dialogs and nothing else then download the Source, cherry-pick the files you need for the Folder browser (hint: 7 files) and it builds fine in .NET 4.5.2. I had to add a reference to System.Drawing. Compare the references in the original project to yours.
How do you figure out which files? Open your app and Ookii in different Visual Studio instances. Add VistaFolderBrowserDialog.cs to your app and keep adding files until the build errors go away. You find the dependencies in the Ookii project - Control-Click the one you want to follow back to its source (pun intended).
Here are the files you need if you're too lazy to do that ...
NativeMethods.cs
SafeHandles.cs
VistaFolderBrowserDialog.cs
\ Interop
COMGuids.cs
ErrorHelper.cs
ShellComInterfaces.cs
ShellWrapperDefinitions.cs
Edit line 197 in VistaFolderBrowserDialog.cs unless you want to include their Resources.Resx
throw new InvalidOperationException(Properties.Resources.FolderBrowserDialogNoRootFolder);
throw new InvalidOperationException("Unable to retrieve the root folder.");
Add their copyright notice to your app as per their license.txt
The code in \Ookii.Dialogs.Wpf.Sample\MainWindow.xaml.cs line 160-169 is an example you can use but you will need to remove this, from MessageBox.Show(this, for WPF.
Works on My Machine [TM]
None of these answers worked for me (generally there was a missing reference or something along those lines)
But this quite simply did:
Using FolderBrowserDialog in WPF application
Add a reference to System.Windows.Forms and use this code:
var dialog = new System.Windows.Forms.FolderBrowserDialog();
System.Windows.Forms.DialogResult result = dialog.ShowDialog();
No need to track down missing packages. Or add enormous classes
This gives me a modern folder selector that also allows you to create a new folder
I'm yet to see the impact when deployed to other machines
I know this is an old question, but a simple way to do this is use the FileDialog option provided by WPF and using System.IO.Path.GetDirectory(filename).
You could use smth like this in WPF. I've created example method.
Check below.
public string getFolderPath()
{
// Create OpenFileDialog
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Multiselect = false;
openFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
if (openFileDialog.ShowDialog() == true)
{
System.IO.FileInfo fInfo = new System.IO.FileInfo(openFileDialog.FileName);
return fInfo.DirectoryName;
}
return null;
}
It seems that the Microsoft.Win32 .NET library does not support selecting folders (only files), so you are out of luck in WPF (as of 7/2022). I feel the best option now is Ookii for WPF: https://github.com/ookii-dialogs/ookii-dialogs-wpf. It works great and as expected in WPF minus Microsoft support. You can get it as a NuGet package. Code behind XAML View:
public partial class ExportRegionView : UserControl
{
public ExportRegionView()
{
InitializeComponent();
}
private void SavePath(object sender, RoutedEventArgs e)
{
var dialog = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog();
dialog.Description = "SIPAS Export Folder";
dialog.UseDescriptionForTitle = true;
if (dialog.ShowDialog().GetValueOrDefault())
{
ExportPath.Text = dialog.SelectedPath;
}
}
}
XAML: <Button Grid.Row="1" Grid.Column="3" Style="{DynamicResource Esri_Button}" Click="SavePath" Margin="5,5,5,5">Path</Button>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Gearplay
{
/// <summary>
/// Логика взаимодействия для OpenFolderBrows.xaml
/// </summary>
public partial class OpenFolderBrows : Page
{
internal string SelectedFolderPath { get; set; }
public OpenFolderBrows()
{
InitializeComponent();
Selectedpath();
InputLogicalPathCollection();
}
internal void Selectedpath()
{
Browser.Navigate(#"C:\");
Browser.Navigated += Browser_Navigated;
}
private void Browser_Navigated(object sender, NavigationEventArgs e)
{
SelectedFolderPath = e.Uri.AbsolutePath.ToString();
//MessageBox.Show(SelectedFolderPath);
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
}
string [] testing { get; set; }
private void InputLogicalPathCollection()
{ // add Menu items for Cotrol
string[] DirectoryCollection_Path = Environment.GetLogicalDrives(); // Get Local Drives
testing = new string[DirectoryCollection_Path.Length];
//MessageBox.Show(DirectoryCollection_Path[0].ToString());
MenuItem[] menuItems = new MenuItem[DirectoryCollection_Path.Length]; // Create Empty Collection
for(int i=0;i<menuItems.Length;i++)
{
// Create collection depend how much logical drives
menuItems[i] = new MenuItem();
menuItems[i].Header = DirectoryCollection_Path[i];
menuItems[i].Name = DirectoryCollection_Path[i].Substring(0,DirectoryCollection_Path.Length-1);
DirectoryCollection.Items.Add(menuItems[i]);
menuItems[i].Click += OpenFolderBrows_Click;
testing[i]= DirectoryCollection_Path[i].Substring(0, DirectoryCollection_Path.Length - 1);
}
}
private void OpenFolderBrows_Click(object sender, RoutedEventArgs e)
{
foreach (string str in testing)
{
if (e.OriginalSource.ToString().Contains("Header:"+str)) // Navigate to Local drive
{
Browser.Navigate(str + #":\");
}
}
}
private void Goback_Click(object sender, RoutedEventArgs e)
{// Go Back
try
{
Browser.GoBack();
}catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void Goforward_Click(object sender, RoutedEventArgs e)
{ //Go Forward
try
{
Browser.GoForward();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void FolderForSave_Click(object sender, RoutedEventArgs e)
{
// Separate Click For Go Back same As Close App With send string var to Main Window ( Main class etc.)
this.NavigationService.GoBack();
}
}
}
I'm using the CAG and I've got some issues using the TabControl region. I worked out that by marking the view (and NOT the PresentationModel) as IActiveAware I can get an event when the view is activated/deactivated. That works well when the composite is simple and the TabItem is the view.
However, in my case I've got a composite inside the TabItem. It can listen to activation events but I'd like to propagate these events to its children so that they can react to them. Is there a way of doing that? I had a look at the RegionContext but it doesn't seem to work in my case (or maybe I'm doing it wrong).
Could it be that I'm missing out on something and an attached dependency or something else would solve my issue?
I decided to use the RegionContext to propagate the IsActive state within the region.
Set it up as:
Regions:RegionManager.RegionContext="{Binding Path=IsActive, Mode=TwoWay}"
on my tab view (which is IActiveAware). Then in the child view I can listen to changes:
RegionContext.GetObservableContext((DependencyObject)View).PropertyChanged += new PropertyChangedEventHandler(VehiclesPresentationModel_PropertyChanged);
private void VehiclesPresentationModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
IsActive = (bool)RegionContext.GetObservableContext((DependencyObject)View).Value;
}
}
The remaining issue was that the reverse would work. Setting IsActive on the tab view doesn't active the tab :(
I added a custom behavior and now it works. The custom bahavior is like:
public class RegionReverseActiveAwareBehavior : RegionBehavior
{
public const string BehaviorKey = "RegionReverseActiveAwareBehavior";
protected override void OnAttach()
{
Region.Views.CollectionChanged += new NotifyCollectionChangedEventHandler(Views_CollectionChanged);
}
private void Views_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
IActiveAware activeAwareItem = item as IActiveAware;
if (activeAwareItem != null)
{
activeAwareItem.IsActiveChanged += new EventHandler(activeAwareItem_IsActiveChanged);
}
}
}
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
IActiveAware activeAwareItem = item as IActiveAware;
if (activeAwareItem != null)
{
activeAwareItem.IsActiveChanged -= new EventHandler(activeAwareItem_IsActiveChanged);
}
}
}
}
private void activeAwareItem_IsActiveChanged(object sender, EventArgs e)
{
IActiveAware activeAware = sender as IActiveAware;
if (activeAware != null &&
activeAware.IsActive)
{
Region.Activate(activeAware);
}
}
}
And then I set it up on the TabControl with:
RegionManager.GetObservableRegion(tabRegion).PropertyChanged +=
(sender, args) =>
{
if (args.PropertyName == "Value")
{
IRegion region = RegionManager.GetObservableRegion(tabRegion).Value;
region.Behaviors.Add(RegionReverseActiveAwareBehavior.BehaviorKey, new RegionReverseActiveAwareBehavior());
}
};
Hope that solves someone else's issue. Or maybe there's an easier way that I'm missing.
Have you looked at Prism EventAggregator? This can be implemented as some sort of MessageBus or Mediator...
You can Puplish events and everyone who needs be interested can subscribe to it...
If you look in the prism samples you find an implementation or something in the docs...
The Prism EventAggregator (EA) object allows you to publish events and subscribe to them through the EA object. This can be used with a single publisher and 0, 1 or many subscribers. I generally use the EA when I need to communicate between different parts of an application that are not tied together. For example, a menu item in the shell of a Prism application may need to invoke another view in a different module. The EA allows you to do this via pub/sub. However if a screen needs to make something happen on its own self, this ifs often better suited for the Command object.
I'm trying to write a ParentAdapter implementation; I'm interested in providing design-time support for some WPF controls I'm writing and this is how you manage custom logic for reparenting items to different container controls. I started small, with the notion of creating a StackPanel-derived class that would only allow Button elements to be parented at design-time (yes, I'm aware the panel itself needs code to support this as well.) I started with what I figured would be the simplest the ParentAdapter could be:
using System;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Windows.Design.Interaction;
using Microsoft.Windows.Design.Model;
namespace ControlLibrary.Design
{
internal class SimplePanelParentAdapter : ParentAdapter
{
public override bool CanParent(ModelItem parent, Type childType)
{
return (childType == typeof(Button));
}
// moves the child item into the target panel; in this case a SimplePanel
public override void Parent(ModelItem newParent, ModelItem child)
{
using (ModelEditingScope undoContext = newParent.BeginEdit())
{
// is this correct?
//child.Content.SetValue("I'm in a custom panel!");
SimplePanel pnl = newParent.GetCurrentValue() as SimplePanel;
pnl.Children.Add(child.GetCurrentValue() as UIElement);
undoContext.Complete();
}
}
public override void RemoveParent(ModelItem currentParent, ModelItem newParent, ModelItem child)
{
// No special things need to be done, right?
child.Content.SetValue("I was in a custom panel.");
}
}
}
When I work with this at design-time, as soon as I drag a button over my custom panel, a NullReferenceException is thrown from deep within the VS code. My code is not throwing the exception, because I can step all the way through my method; the call stack indicates that code in Microsoft.Windows.Design.Developer.dll is throwing the exception.
Obviously I'm doing something incorrectly, but the documentation provides no examples and my search-fu seems to indicate that either no one is trying this or anyone who is trying it isn't talking about it. Does anyone have suggestions?
I found the answer to my question myself. The problem is caused by editing the model instead of the ModelItem wrapper. What I should have done (and does work) is something like this:
using System;
using System.Windows.Controls;
using Microsoft.Windows.Design.Interaction;
using Microsoft.Windows.Design.Model;
namespace ControlLibrary.Design
{
internal class SimplePanelParentAdapter : ParentAdapter
{
public override bool CanParent(ModelItem parent, Type childType)
{
return (childType == typeof(Button));
}
// moves the child item into the target panel; in this case a SimplePanel
public override void Parent(ModelItem newParent, ModelItem child)
{
using (ModelEditingScope undoContext = newParent.BeginEdit())
{
ModelProperty prop = newParent.Properties["Children"];
ModelItemCollection items = (ModelItemCollection)prop.Value;
items.Add(child);
undoContext.Complete();
}
}
public override void RemoveParent(ModelItem currentParent, ModelItem newParent, ModelItem child)
{
using (ModelEditingScope scope = child.BeginEdit())
{
ModelProperty prop = currentParent.Properties["Children"];
((ModelItemCollection)prop.Value).Remove(child);
scope.Complete();
}
}
}
}
I was confused when I wrote the first code and unsure how I was supposed to call Add() on the Children property; it looks like ModelProperty.Value wraps collections with ModelItemCollection, so unless you go out of your way to make your class use an obtuse interface this should work.
How are you instrumenting your UI's? In the past I've read that people have instrumented their user interfaces, but what I haven't found is examples or tips on how to instrument a UI.
By instrumenting, I mean collecting data regarding usage and performance of the system. A MSDN article on Instrumentation is http://msdn.microsoft.com/en-us/library/x5952w0c.aspx. I would like to capture which buttons users click on, what keyboard shortucts they use, what terms they use to search, etc.
How are you instrumenting your UI?
What format are you storing the instrumentation?
How are you processing the instrumented data?
How are you keeping your UI code clean with this instrumentation logic?
Specifically, I am implementing my UI in WPF, so this will provide extra challenges compared to instrumenting a web-based application. (i.e. need to transfer the instrumented data back to a central location, etc). That said, I feel the technology may provide an easier implementation of instrumentation via concepts like attached properties.
Have you instrumented a WPF application? Do you have any tips on how this can be achieved?
Edit: The following blog post presents an interesting solution: Pixel-In-Gene Blog: Techniques for UI Auditing on WPF apps
Here is an example of how I use a simple events manager to hook on to the UI events and extract key information of the events, such as name and type of UI element, name of event and the parent window's type name. For lists I also extract the selected item.
This solution only listens for clicks of controls derived from ButtonBase (Button, ToggleButton, ...) and selection changes in controls derived from Selector (ListBox, TabControl, ...). It should be easy to extend to other types of UI elements or to achieve a more fine-grained solution. The solution is inspired of Brad Leach's answer.
public class UserInteractionEventsManager
{
public delegate void ButtonClickedHandler(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowName);
public delegate void SelectorSelectedHandler(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowName, object selectedObject);
public event ButtonClickedHandler ButtonClicked;
public event SelectorSelectedHandler SelectorSelected;
public UserInteractionEventsManager()
{
EventManager.RegisterClassHandler(typeof(ButtonBase), ButtonBase.ClickEvent, new RoutedEventHandler(HandleButtonClicked));
EventManager.RegisterClassHandler(typeof(Selector), Selector.SelectionChangedEvent, new RoutedEventHandler(HandleSelectorSelected));
}
#region Handling events
private void HandleSelectorSelected(object sender, RoutedEventArgs e)
{
// Avoid multiple events due to bubbling. Example: A ListBox inside a TabControl will cause both to send the SelectionChangedEvent.
if (sender != e.OriginalSource) return;
var args = e as SelectionChangedEventArgs;
if (args == null || args.AddedItems.Count == 0) return;
var element = sender as FrameworkElement;
if (element == null) return;
string senderName = GetSenderName(element);
string parentWindowName = GetParentWindowTypeName(sender);
DateTime time = DateTime.Now;
string eventName = e.RoutedEvent.Name;
string senderTypeName = sender.GetType().Name;
string selectedItemText = args.AddedItems.Count > 0 ? args.AddedItems[0].ToString() : "<no selected items>";
if (SelectorSelected != null)
SelectorSelected(time, eventName, senderName, senderTypeName, parentWindowName, selectedItemText);
}
private void HandleButtonClicked(object sender, RoutedEventArgs e)
{
var element = sender as FrameworkElement;
if (element == null) return;
string parentWindowName = GetParentWindowTypeName(sender);
DateTime time = DateTime.Now;
string eventName = e.RoutedEvent.Name;
string senderTypeName = sender.GetType().Name;
string senderName = GetSenderName(element);
if (ButtonClicked != null)
ButtonClicked(time, eventName, senderName, senderTypeName, parentWindowName);
}
#endregion
#region Private helpers
private static string GetSenderName(FrameworkElement element)
{
return !String.IsNullOrEmpty(element.Name) ? element.Name : "<no item name>";
}
private static string GetParentWindowTypeName(object sender)
{
var parent = FindParent<Window>(sender as DependencyObject);
return parent != null ? parent.GetType().Name : "<no parent>";
}
private static T FindParent<T>(DependencyObject item) where T : class
{
if (item == null)
return default(T);
if (item is T)
return item as T;
DependencyObject parent = VisualTreeHelper.GetParent(item);
if (parent == null)
return default(T);
return FindParent<T>(parent);
}
#endregion
}
And to do the actual logging, I use log4net and created a separate logger named 'Interaction' to log user interaction. The class 'Log' here is simply my own static wrapper for log4net.
/// <summary>
/// The user interaction logger uses <see cref="UserInteractionEventsManager"/> to listen for events on GUI elements, such as buttons, list boxes, tab controls etc.
/// The events are then logged in a readable format using Log.Interaction.Info().
/// </summary>
public class UserInteractionLogger
{
private readonly UserInteractionEventsManager _events;
private bool _started;
/// <summary>
/// Create a user interaction logger. Remember to Start() it.
/// </summary>
public UserInteractionLogger()
{
_events = new UserInteractionEventsManager();
}
/// <summary>
/// Start logging user interaction events.
/// </summary>
public void Start()
{
if (_started) return;
_events.ButtonClicked += ButtonClicked;
_events.SelectorSelected += SelectorSelected;
_started = true;
}
/// <summary>
/// Stop logging user interaction events.
/// </summary>
public void Stop()
{
if (!_started) return;
_events.ButtonClicked -= ButtonClicked;
_events.SelectorSelected -= SelectorSelected;
_started = false;
}
private static void SelectorSelected(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowTypeName, object selectedObject)
{
Log.Interaction.Info("{0}.{1} by {2} in {3}. Selected: {4}", senderTypeName, eventName, senderName, parentWindowTypeName, selectedObject);
}
private static void ButtonClicked(DateTime time, string eventName, string senderName, string senderTypeName, string parentWindowTypeName)
{
Log.Interaction.Info("{0}.{1} by {2} in {3}", senderTypeName, eventName, senderName, parentWindowTypeName);
}
}
The output would then look something like this, omitting non-relevant log entries.
04/13 08:38:37.069 INFO Iact ToggleButton.Click by AnalysisButton in MyMainWindow
04/13 08:38:38.493 INFO Iact ListBox.SelectionChanged by ListView in MyMainWindow. Selected: Andreas Larsen
04/13 08:38:44.587 INFO Iact Button.Click by EditEntryButton in MyMainWindow
04/13 08:38:46.068 INFO Iact Button.Click by OkButton in EditEntryDialog
04/13 08:38:47.395 INFO Iact ToggleButton.Click by ExitButton in MyMainWindow
The following blog post gives quite a few good ideas for instrumenting a WPF application:
Techniques for UI Auditing on WPF apps.
You could consider log4net. It is a robust logging framework that exists in a single DLL. It is also done in a "non demanding" type mode so that if a critical process is going on, it won't log until resources are freed up a bit more.
You could easily setup a bunch of INFO level loggers and track all the user interaction you needed, and it wouldn't take a bug crash to send the file to yourself. You could also then log all your ERROR and FATAL code to seperate file that could easily be mailed to you for processing.
If you make use of WPF commands, each custom command could then log the Action taken. You can also log the way the command was initiated.
Perhaps the Microsoft UI Automation for WPF can help out ? Its a framework for automating your UI, perhaps it can be used to log stuff for you...
We use the Automation Framework for auto-testing our UI in WPF.
I have not yet developed using WPF.. But I would assume that its the same as most other applications in that you want to keep the UI code as light as possible.. A number of design patterns may be used in this such as the obvious MVC and Façade. I personally always try and keep the objects travelling between the UI and BL layers as light as possible, keeping them to primitives if I can.
This then helps me focus on improving the UI layer without the concerns of anything going on once I throw my (primitive) data back..
I hope I understood your question correctly, and sorry I cannot offer more contextual help with WPF.
Disclaimer: I work for the company that sells this product, not only that but I am a developer on this particular product :) .
If you are interested in a commercial product to provide this then Runtime Intelligence (a functional add on to Dotfuscator ) that injects usage tracking functionality into your .NET applications is available. We provide not only the actual implementation of the tracking functionality but the data collection, processing and reporting functionality as well.
There was recently a discussion on the Business of Software forum on this topic that I also posted in located here: http://discuss.joelonsoftware.com/default.asp?biz.5.680205.26 .
For a high level overview of our stuff see here: http://www.preemptive.com/runtime-intelligence-services.html .
In addition I am currently working on writing up some more technically oriented documentation as we realize that is an area we could definitely improve, please let me know if anyone is interested in being notified when I have completed it.