How to await the generation of containers in an ItemsControl? - c#

I have a SettingsWindow, in it there is an audio file selector which has a context menu. Some code accesses the MyAudioFileSelector computed property before it can get the AudioFileSelector because the AudioFileSelector is just inside a DataTemplate of an item in an ItemsControl that has not yet generated its containers at that moment. I tried to defer the access to MyAudioFileSelector using Dispatcher.BeginInvoke with DispatcherPrority.Loaded, but the item containers are still not yet generated at that moment.
The code that accesses the MyAudioFileSelector is the method that applies one of the many settings inside the user-selected data file. This method is called from the Window's Loaded event handler synchronously for each setting in the program's data files' schema.
I am very new to async-await programming, I have read this but I am not sure how this helps me, and I read this page but I am still not sure what to do. I have read this a but the only answer, unaccepted, seems similar to what I already use below:
MySettingsWindow.Dispatcher.BeginInvoke(new Action(() =>
{
[...]
}), System.Windows.Threading.DispatcherPriority.Loaded);
A part of the XAML
(InverseBooleanConv just makes true, false, and false, true)
<ItemsControl Grid.ColumnSpan="3" Margin="0,0,-0.6,0" Grid.Row="0"
ItemsSource="{Binding SettingsVMs}" x:Name="MyItemsControl">
<ItemsControl.Resources>
<xceed:InverseBoolConverter x:Key="InverseBooleanConv"/>
<DataTemplate DataType="{x:Type local:AudioFileSettingDataVM}">
<local:AudioFileSelector MaxHeight="25" Margin="10" FilePath="{Binding EditedValue, Mode=TwoWay}">
<local:AudioFileSelector.RecentAudioFilesContextMenu>
<local:RecentAudioFilesContextMenu
PathValidationRequested="RecentAudioFilesContextMenu_PathValidationRequested"
StoragePropertyName="RecentAudioFilePaths"
EmptyLabel="No recent audio files."/>
</local:AudioFileSelector.RecentAudioFilesContextMenu>
</local:AudioFileSelector>
</DataTemplate>
[...]
Parts of the code-behind
In MainWindow.xaml.cs, the beginning of the Window_Loaded handler
private void Window_Loaded(object sender, RoutedEventArgs e)
{
VM.ClockVMCollection.Model.FiltersVM.Init();
VM.Settings.IsUnsavedLocked = true;
VM.ClockVMCollection.Model.IsUnsavedLocked = true;
foreach (KeyValuePair<string, SettingDataM> k in VM.Settings)
{
ApplySetting(k.Value);
}
[...]
In MainWindow.xaml.cs, in the method ApplySetting
case "AlwaysMute":
VM.MultiAudioPlayer.Mute = (bool)VM.Settings.GetValue("AlwaysMute");
break;
case "RecentAudioFilePaths":
MySettingsWindow.Dispatcher.BeginInvoke(new Action(() =>
{
MySettingsWindow.MyRecentAudioFilesContextMenu. // here, MyRecentAudioFilesContextMenu is null, this is the problem
LoadRecentPathsFromString(VM.Settings.GetValue("RecentAudioFilePaths") as string);
}), System.Windows.Threading.DispatcherPriority.Loaded);
break;
case "RecentImageFilePaths":
MySettingsWindow.Dispatcher.BeginInvoke(new Action(() =>
{
MySettingsWindow.MyRecentImageFilesContextMenu. // here, MyRecentImageFilesContextMenu is null, this is the problem
LoadRecentPathsFromString(
VM.Settings.GetValue("RecentImageFilePaths") as string);
}), System.Windows.Threading.DispatcherPriority.Loaded);
break;
[...]
In the SettingsWindow class
internal AudioFileSelector MyAudioFileSelector
{
get
{
foreach (SettingDataVM vm in MyItemsControl.ItemsSource)
{
if (vm is AudioFileSettingDataVM)
{
return (AudioFileSelector)MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm);
}
}
return null;
}
}
internal ImageFileSelector MyImageFileSelector
{
get
{
foreach (SettingDataVM vm in MyItemsControl.ItemsSource)
{
if (vm is ImageFileSettingDataVM)
{
return (ImageFileSelector)MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm);
}
}
return null;
}
}
internal RecentAudioFilesContextMenu MyRecentAudioFilesContextMenu
{
get
{
return MyAudioFileSelector?.RecentAudioFilesContextMenu;
}
}
internal RecentFilesContextMenu MyRecentImageFilesContextMenu
{
get
{
return MyImageFileSelector?.RecentImageFilesContextMenu;
}
}
The bug is in the two C# comments in one of the code snippets above, null reference exceptions.
I think I could attach in the MainWindow a handler to SettingsWindow's ItemsControl's ItemContainerGenerator's StatusChanged event and then continue the initialization of the window, including the loading of all the settings, but I wonder if there is a more orderly/correct way.
Thank you.

If you have access to your ItemsControl in the code-behind under the variable name MyItemsControl, then you can add an event handler for the ContainerGenerator StatusChanged event:
private void Window_Loaded(object sender, RoutedEventArgs e) {
//Subscribe to generated containers event of the ItemsControl
MyItemsControl.ItemContainerGenerator.StatusChanged += ContainerGenerator_StatusChanged;
}
/// <summary>
/// Handles changed in container generator status.
///</summary>
private void ContainerGenerator_StatusChanged(object sender, EventArgs e) {
var generator = sender as ItemContainerGenerator;
//Check that containers have been generated
if (generator.Status == GeneratorStatus.ContainersGenerated ) {
//Do stuff
}
}
I really recommand not to use this if what you're after is simply save/load data from a file, as they are completely unrelated.

Related

Update ThemeResources from c#?

I've got a void that analyses an image, extracts two dominant colors, and replaces "SystemControlForegroundAccentBrush" and "SystemControlHighlightAccentBrush", that I'm overriding in App.xaml. It works well, except that it takes a bit of time to analyse the image, so it changes the colors once all the controls in my page have already loaded.
As a result, they stay the old accent color. How can I get them to bind dynamically to that ThemeRessource?
Here's my app.xaml:
<Application.Resources>
<ResourceDictionary x:Name="resdic">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SystemControlForegroundAccentBrush"/>
<SolidColorBrush x:Key="SystemControlHighlightAccentBrush"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
This is the (very simplified) part of the ColorExtractor.cs class that changes the colors:
public async static void Analyse()
{
Application.Current.Resources["SystemControlForegroundAccentBrush"] = new SolidColorBrush(color);
Application.Current.Resources["SystemControlHighlightAccentBrush"] = new SolidColorBrush(color2);
}
And I've got a bunch of controls in Page.xaml that have their Foreground set as such:
<TextBlock Foreground="{ThemeResource SystemControlForegroundAccentBrush]"/>
I call ColorExtractor.Analyse() in my Page.xaml.cs (at the OnNavigatedTo event). I can always create an event that gets fired once the colors are set, but I need to find a way to update the colors of all the controls in my page once that's done.
You can have a look at how Template 10 does theming changes, but in their case they are defining two different themes in resource dictionaries in advance. You can find their code in the repo on Github, but here are some of the code used:
(Window.Current.Content as FrameworkElement).RequestedTheme = value.ToElementTheme();
Views.Shell.SetRequestedTheme(value, UseBackgroundChecked);
From here
public static void SetRequestedTheme(ApplicationTheme theme, bool UseBackgroundChecked)
{
WindowWrapper.Current().Dispatcher.Dispatch(() =>
{
(Window.Current.Content as FrameworkElement).RequestedTheme = theme.ToElementTheme();
ParseStyleforThemes(theme);
HamburgerMenu.NavButtonCheckedForeground = NavButtonCheckedForegroundBrush;
HamburgerMenu.NavButtonCheckedBackground = (UseBackgroundChecked) ?
NavButtonCheckedBackgroundBrush : NavButtonBackgroundBrush;
HamburgerMenu.NavButtonCheckedIndicatorBrush = (UseBackgroundChecked) ?
Colors.Transparent.ToSolidColorBrush() : NavButtonCheckedIndicatorBrush;
HamburgerMenu.SecondarySeparator = SecondarySeparatorBrush;
List<HamburgerButtonInfo> NavButtons = HamburgerMenu.PrimaryButtons.ToList();
NavButtons.InsertRange(NavButtons.Count, HamburgerMenu.SecondaryButtons.ToList());
List<HamburgerMenu.InfoElement> LoadedNavButtons = new List<HamburgerMenu.InfoElement>();
foreach (var hbi in NavButtons)
{
StackPanel sp = hbi.Content as StackPanel;
if (hbi.ButtonType == HamburgerButtonInfo.ButtonTypes.Literal) continue;
ToggleButton tBtn = sp.Parent as ToggleButton;
Button btn = sp.Parent as Button;
if (tBtn != null)
{
var button = new HamburgerMenu.InfoElement(tBtn);
LoadedNavButtons.Add(button);
}
else if (btn != null)
{
var button = new HamburgerMenu.InfoElement(btn);
LoadedNavButtons.Add(button);
continue;
}
else
{
continue;
}
Rectangle indicator = tBtn.FirstChild<Rectangle>();
indicator.Visibility = ((!hbi.IsChecked ?? false)) ? Visibility.Collapsed : Visibility.Visible;
if (!hbi.IsChecked ?? false) continue;
ContentPresenter cp = tBtn.FirstAncestor<ContentPresenter>();
cp.Background = NavButtonCheckedBackgroundBrush;
cp.Foreground = NavButtonCheckedForegroundBrush;
}
LoadedNavButtons.ForEach(x => x.RefreshVisualState());
});
}
From here
I've got a void that analyses an image, extracts two dominant colors, and replaces "SystemControlForegroundAccentBrush" and "SystemControlHighlightAccentBrush", that I'm overriding in App.xaml.
First I don't think your code in app.xaml can override the ThemeResource, and you used this Brush in the Page like this:
<TextBlock Foreground="{ThemeResource SystemControlForegroundAccentBrush}" Text="Hello World!" FontSize="50" />
If you press "F12" on the SystemControlForegroundAccentBrush, you can find this resource actually in the "generic.xaml" file.
Now suppose your ColorExtractor.cs class works fine and ColorExtractor.Analyse() can override the color of those two brushes, and you have many controls in the page uses these two resources, refreshing the Page here can solve your problem.
But I think it's better that not put this operation in OnNavagateTo event or Page.Loaded event, there is no refresh method in UWP, we use navigating again to this page to refresh, so if putting this operation in OnNavagateTo event or Page.Loaded event, every time you navigate to this page, the resources will be overrided and it will navigate again. So I put this operation in a Button click event like this:
public bool Reload()
{
return Reload(null);
}
private bool Reload(object param)
{
Type type = this.Frame.CurrentSourcePageType;
if (this.Frame.BackStack.Any())
{
type = this.Frame.BackStack.Last().SourcePageType;
param = this.Frame.BackStack.Last().Parameter;
}
try { return this.Frame.Navigate(type, param); }
finally
{
this.Frame.BackStack.Remove(this.Frame.BackStack.Last());
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ColorExtractor.Analyse();
Reload();
}
In the end, I decided to create an event in ColorExtractor.cs :
public static event EventHandler Analysed;
public async static void Analyse(BitmapImage poster)
{
//Analyse colors
Analysed(null, null);
}
And then, on my MainPage.xaml.cs:
ColorExtractor.Analyse(bmp);
ColorExtractor.Analysed += (sender, EventArgs) =>
{
//Set Page foreground color, as a lot of controls are dynamically binded to their parent's foreground brush.
//If a control isn't automatically binded, all I have to do is say: Foreground="{Binding Foreground, ElementName=page}"
page.Foreground = Application.Current.Resources["SystemControlForegroundAccentBrush"] as SolidColorBrush;
page.BorderBrush = Application.Current.Resources["SystemControlHighlightAccentBrush"] as SolidColorBrush;
//Reload any custom user control that sets it's children's color when it's loaded.
backdrop.UserControl_Loaded(null, null);
};
So I'm not actually binding my controls to the ForegroundAccentBrush directly, but this works without needing to re-navigate to the page.

Selecting A Series Of Text Boxes

I have a series(10) of selectable TextBoxes. I need to be able to click on one of them and select all the text boxes on which the mouse is moved until the click is released.
I used the following code but I am unable to hit the MouseMove on the other TextBoxes. It always hits the TextBox on which the Click was made.
class SelectableTextBox: TextBox
{
public Boolean IsSelected { get; protected set; }
public void select(Boolean value)
{
this.IsSelected = value;
if (value)
{
this.Background = System.Windows.Media.Brushes.Aqua;
}
else
{
this.Background = System.Windows.Media.Brushes.White;
}
}
}
private void onPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = !textBox.IsSelected;
textBox.select(!textBox.IsSelected);
}
private void onPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
if (this.SelectionStartedRight)
{
textBox.select(true);
}
}
private void onPreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = false;
}
Try using MouseEnter event instead of MouseMove. Attach MouseEnter to your selectable textboxes. That would ensure that only they trigger the desired event handler and its code.
If you decide to stay with the global handler, be careful when you convert sender to a specific control type. You need to account for those times when it's not the "expected" control:
SelectableTextBox textBox = sender as SelectableTextBox;
if (textBox != null)
{
// The rest of the code here...
}
select is a Linq keyword, so you might want to rename that method to avoid any conflicts down the road. While I don't think it'd be causing any issues, I would change it.
You don't have to follow this convention, but in C# its customary to use an uppercase for the first letter of a method. Also note that the default access modifier for a class is internal... just want to make sure you're aware of that, as you continue your development.
Your last method, onPreviewMouseLeftButtonUp(...) has the following code in it:
SelectableTextBox textBox = (SelectableTextBox)sender;
Not only is it unsafe, as I've described above, but it also does absolutely nothing.
Lastly... and this is just me, I would probably move the code that handles changing the state of a selectable textbox (selected or not selected) into its class, since it belongs there. Why should anything else be in charge of how it handles its state. Keep things where they belong and you'll have a much easier time testing, debugging and maintaining your code. Don't fall into the "I'll refactor it later" trap... it'll rarely happen.
Here's my crude example. I let the class handle its MouseEnter event and simply check if the Mouse.LeftButton is down at that time. You would have to expand on it, but it should be a solid start:
Made some edits per OP's requests in the comments.
Preview:
XAML:
<Window x:Class="SelectableTextBoxes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SelectableTextBoxes"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
</StackPanel>
</Window>
C# (Pardon me for putting the SelectableTextBox into the same file...):
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SelectableTextBoxes
{
// Move this class into its own file (it's here for prototyping).
public class SelectableTextBox : TextBox
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
// If the value changes, sets new color.
SetSelectionColor();
}
}
}
public SelectableTextBox()
{
// For handling an initial click if it happens in the textbox.
PreviewMouseDown += SelectableTextBox_PreviewMouseDown;
// For handling selection when mouse enters the textbox
// and left mouse button is down.
MouseEnter += SelectableTextBox_MouseEnter;
// To handle mouse capture (release it).
GotMouseCapture += SelectableTextBox_GotMouseCapture;
}
// Handles the mouse down event within the textbox.
void SelectableTextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!IsSelected)
{
IsSelected = true;
}
// If one of the Shift keys is down, return, since
// we don't want to deselect others.
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
return;
}
// This part makes a poor assumption that the parent is
// always going to be a Panel... expand on this code to
// cover other types that may contain more than one element.
var parent = VisualTreeHelper.GetParent(this) as Panel;
if (parent != null)
{
foreach (var child in parent.Children)
{
// If a child is not of a correct type, it'll be null.
var tbx = child as SelectableTextBox;
// This is where we check to see if it's null or this instance.
if (tbx != null && tbx != this)
{
tbx.IsSelected = false;
}
}
}
}
// When textbox receives focus, this event fires... we need to release
// the mouse to continue selection.
void SelectableTextBox_GotMouseCapture(object sender, MouseEventArgs e)
{
ReleaseMouseCapture();
}
// Sets selection state to true if the left mouse button is
// down while entering.
void SelectableTextBox_MouseEnter(object sender, MouseEventArgs e)
{
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
IsSelected = true;
}
}
// Sets the background color based on selection state.
private void SetSelectionColor()
{
if (IsSelected)
{
Background = Brushes.LightCyan;
}
else
{
Background = Brushes.White;
}
}
}
// Window code... should be on its own, but I placed the two
// classes together while prototyping.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

ComboBox Up/Down arrow keys issue after Items is repopulated

I am creating a custom ComboBox control with Google Search results implementation but I am running into some problem using the arrow keys.
Details of the problem
Please watch this video as I demonstrate the problem with UP and DOWN arrow keys and take a look on the output panel as well.
http://screencast.com/t/DFkmlDKR
In the video, I tried searching for "a" (just a test) and returns the Google search results that starts with "a" then I am pressing the DOWN and UP arrow keys. In the output panel, it shows the highlighted item as I press these keys.
Then I typed the next letter "l". Now If you noticed on the output panel, the selection is now "null" but I'm still pressing the UP & DOWN arrow keys.
And when you hover on the item again using the mouse pointer, it will start working again.
I am stuck with this problem for days and haven't figured out the solution.
I have uploaded the test version of this control so you can play with it as well. Here it is GoogleSuggestionComboBox
My goal here is to make the UP & DOWN arrow keys work all the time.
In this section of the code
I tried adding
SelectedIndex = 0;
after the ForEach statement so everytime the collection is repopulated with the new results, it will select the first result. Unfortunately, it did not work.
You can download the test code so you can play and test the problem. http://sdrv.ms/1eWV3Bc and here's the code for the ComboBox as well.
using GoogleSuggestionComboBox.Model;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace GoogleSuggestionComboBox
{
public class ComboBoxExC : ComboBox
{
GoogleSuggest google;
TextBox textbox;
string _text = string.Empty;
string _last_text = string.Empty;
public ComboBoxExC()
{
if (DesignerProperties.GetIsInDesignMode(this))
{
}
else
{
this.Loaded += ComboBoxExC_Loaded;
google = new GoogleSuggest();
google.OnGoogleSuggestAvailable += google_OnGoogleSuggestAvailable;
// since we have OnSelectionChanged "disabled"
// we need a way to know if the item in ComboBox is selected using mouse
EventManager.RegisterClassHandler(typeof(ComboBoxItem), ComboBoxItem.MouseDownEvent, new MouseButtonEventHandler(OnItemMouseDown));
}
}
void ComboBoxExC_Loaded(object sender, RoutedEventArgs e)
{
this.textbox = (TextBox)Template.FindName("PART_EditableTextBox", this);
}
private void OnItemMouseDown(object sender, MouseButtonEventArgs e)
{
var comboBoxItem = sender as ComboBoxItem;
if (comboBoxItem != null && comboBoxItem.IsHighlighted)
{
Model_SuggestedQueries m = (Model_SuggestedQueries)comboBoxItem.Content;
Go(m.Query);
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
// don't do anything so the .Text value won't change.
//base.OnSelectionChanged(e);
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
this.IsDropDownOpen = true;
base.OnPreviewKeyDown(e);
d("key: " + e.Key.ToString());
if (this.SelectedItem != null)
{
Model_SuggestedQueries m = (Model_SuggestedQueries)this.SelectedItem;
d("selected: " + m.Query);
}
else
{
d("null");
}
}
protected override void OnPreviewKeyUp(KeyEventArgs e)
{
base.OnPreviewKeyUp(e);
if (e.Key == Key.Enter)
{
if (this.SelectedItem == null)
{
Go(this.Text);
}
else
{
Model_SuggestedQueries m = (Model_SuggestedQueries)this.SelectedItem;
Go(m.Query);
}
}
else
{
if (this.Text != this._last_text)
{
google.LookForSuggestion(this.Text);
this._last_text = this.Text;
}
}
}
void google_OnGoogleSuggestAvailable(object sender, List<Model.Model_SuggestedQueries> suggestions)
{
this.Items.Clear();
suggestions.ForEach((a) =>
{
this.Items.Add(a);
});
}
void d(object a)
{
Debug.WriteLine(">>> " + a);
}
void Go(string query)
{
Process.Start("https://www.google.com.ph/search?q=" + query);
// clear suggestions
this.Items.Clear();
}
}
}
MainWindow.xaml
<Window x:Class="GoogleSuggestionComboBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:GoogleSuggestionComboBox"
Title="MainWindow" Height="133" Width="261" WindowStartupLocation="CenterScreen"
>
<Grid>
<StackPanel Margin="10">
<TextBlock Text="Search" />
<l:ComboBoxExC
IsEditable="True"
IsTextSearchEnabled="False"
TextSearch.TextPath="Query"
>
<l:ComboBoxExC.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Query}" />
</DataTemplate>
</l:ComboBoxExC.ItemTemplate>
</l:ComboBoxExC>
</StackPanel>
</Grid>
</Window>
Thank you,
Jayson
This is a pretty old question but I ran into about the same problem. The main issue for me was after selecting an item or clearing selection (setting it to -1) I could not go through the items with the arrow keys anymore until I hovered over them with my mouse.
After some days of struggling I found a fix for this:
KeyboardNavigation.SetDirectionalNavigation( this, KeyboardNavigationMode.Cycle );
this is the combobox, and this line is in the constructor of a sub classed combobox.
I don't know the exact issue, but here is a suggestion:
Don't subclass the ComboBox - you don't need to.
Just drop one on the MainForm, and then work with it from the code behind - it is much easier to work with it like that. You only need to subclass if you need to change the way a combo-box works which is not typical.
I tried downloading the test project, but it is not a zip file - it is a .7z which I could not open.
Also, you main form is a Window. That may not be an issue, but try creating a new project and just working with the Main form the project gives you. There is some wiring up that is done in the App that calls the MainForm that will catch exceptions etc, and that may give you more data as to what the issue is.
Greg

Porting WinForms drag and drop to WPF drag and drop

I am porting my program from WinForms to WPF and have ran into some issues with the drag and drop. It should allow for dragging from a TreeView (it is like a file explorer) to a textbox which opens the file. However, the WPF version acts like a copy-and-paste of the TreeViewItem's header text automatically. I think I just have something mixed up? Possibly the DataObject stuff.
The fully functional, relevant WinForms code:
private void treeView1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
TreeNode node = treeView1.GetNodeAt(e.Location);
if (node != null) treeView1.DoDragDrop(node, DragDropEffects.Move);
}
textbox[i].DragDrop += (o, ee) =>
{
if (ee.Data.GetDataPresent(typeof(TreeNode)))
{
TreeNode node = (TreeNode)ee.Data.GetData(typeof(TreeNode));
((Textbox)o).Text = File.ReadAllLines(pathRoot + node.Parent.FullPath);
...
The WPF code that should do the same thing:
private void TreeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
TreeViewItem item = e.Source as TreeViewItem;
if (item != null)
{
DataObject dataObject = new DataObject();
dataObject.SetData(DataFormats.StringFormat, GetFullPath(item));
DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
}
}
//textbox[i].PreviewDrop += textbox_Drop;
private void textbox_Drop(object sender, DragEventArgs e)
{
TreeViewItem node = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem)); //null?
((Textbox)sender).Text = "";
//this is being executed BUT then the node's header text is being pasted
//also, how do I access the DataObject I passed?
}
Problem: In my WPF version, I am setting the textbox's text to empty (as a test), which occurs, but afterwards the TreeViewItem's header text is being pasted which is not what I want.
Questions: What is the correct way to port this WinForms code to WPF? Why is the text being pasted in the WPF version? How do I prevent that? Am I using the correct events? How do I access the DataObject in textbox_Drop so that I can open the file like I did in the WinForms version? Why is TreeViewItem node always null in the WPF version?
Ah, what the heck, I'll expand my comment to an answer:
The link to read, as mentioned, is this:
http://msdn.microsoft.com/en-us/library/hh144798.aspx
Short story, the TextBox-derived controls already implement most of the "guts" for basic drag/drop operations, and it is recommended that you extend that rather than provide explicit DragEnter/DragOver/Drop handlers.
Assuming a tree "data" structure like:
public class TreeThing
{
public string Description { get; set; }
public string Path { get; set; }
}
The handlers might look something like this:
this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
{
e.Effects = !e.Data.GetDataPresent("treeThing") ?
DragDropEffects.None :
DragDropEffects.Copy;
}), true);
this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
{
if (e.Data.GetDataPresent("treeThing"))
{
var item = e.Data.GetData("treeThing") as TreeThing;
if (item != null)
{
tb.Text = item.Path;
// TODO: Actually open up the file here
}
}
}), true);
And just for giggles, here's a quick-and-dirty test app that is pure showboating in it's use of the Reactive Extensions (Rx) for the drag start stuff:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TreeView x:Name="tree" Grid.Column="0" ItemsSource="{Binding TreeStuff}" DisplayMemberPath="Description"/>
<TextBox x:Name="tb" Grid.Column="1" AllowDrop="True" Text="Drop here" Height="30"/>
</Grid>
</Window>
Nasty code-behind (too lazy to MVVM this):
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
TreeStuff = new ObservableCollection<TreeThing>()
{
new TreeThing() { Description="file 1", Path = #"c:\temp\test.txt" },
new TreeThing() { Description="file 2", Path = #"c:\temp\test2.txt" },
new TreeThing() { Description="file 3", Path = #"c:\temp\test3.txt" },
};
var dragStart =
from mouseDown in
Observable.FromEventPattern<MouseButtonEventHandler, MouseEventArgs>(
h => tree.PreviewMouseDown += h,
h => tree.PreviewMouseDown -= h)
let startPosition = mouseDown.EventArgs.GetPosition(null)
from mouseMove in
Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
h => tree.MouseMove += h,
h => tree.MouseMove -= h)
let mousePosition = mouseMove.EventArgs.GetPosition(null)
let dragDiff = startPosition - mousePosition
where mouseMove.EventArgs.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(dragDiff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(dragDiff.Y) > SystemParameters.MinimumVerticalDragDistance)
select mouseMove;
dragStart.ObserveOnDispatcher().Subscribe(start =>
{
var nodeSource = this.FindAncestor<TreeViewItem>(
(DependencyObject)start.EventArgs.OriginalSource);
var source = start.Sender as TreeView;
if (nodeSource == null || source == null)
{
return;
}
var data = (TreeThing)source
.ItemContainerGenerator
.ItemFromContainer(nodeSource);
DragDrop.DoDragDrop(nodeSource, new DataObject("treeThing", data), DragDropEffects.All);
});
this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
{
e.Effects = !e.Data.GetDataPresent("treeThing") ?
DragDropEffects.None :
DragDropEffects.Copy;
}), true);
this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
{
if (e.Data.GetDataPresent("treeThing"))
{
var item = e.Data.GetData("treeThing") as TreeThing;
if (item != null)
{
tb.Text = item.Path;
// TODO: Actually open up the file here
}
}
}), true);
this.DataContext = this;
}
private T FindAncestor<T>(DependencyObject current)
where T:DependencyObject
{
do
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
}
while (current != null);
return null;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<TreeThing> TreeStuff { get; set; }
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class TreeThing
{
public string Description { get; set; }
public string Path { get; set; }
}
}
You've got more than one problem, enough to make this difficult. First issue is that you got the drag object wrong, you are dragging a string but still checking for a TreeViewItem. Just use the same approach as you used in Winforms, dragging the node. Second problem is that TextBox already implements D+D support and that gets in the way of your code. And the reason you saw the text show up after the drop.
Let's tackle the start of the drag first. You'll need to do a bit of extra work since the way you started the drag interferes with the normal usage of the TreeView, it gets very hard to select a node. Only start the drag when the mouse was moved far enough:
private Point MouseDownPos;
private void treeView1_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
MouseDownPos = e.GetPosition(treeView1);
}
private void treeView1_PreviewMouseMove(object sender, MouseEventArgs e) {
if (e.LeftButton == MouseButtonState.Released) return;
var pos = e.GetPosition(treeView1);
if (Math.Abs(pos.X - MouseDownPos.X) >= SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(pos.Y - MouseDownPos.Y) >= SystemParameters.MinimumVerticalDragDistance) {
TreeViewItem item = e.Source as TreeViewItem;
if (item != null) DragDrop.DoDragDrop(item, item, DragDropEffects.Copy);
}
}
Now the drop, you will need to implement the DragEnter, DragOver and Drop event handlers to avoid the default D+D support built into TextBox from getting in the way. Setting the e.Handled property to true is necessary:
private void textBox1_PreviewDragEnter(object sender, DragEventArgs e) {
if (e.Data.GetDataPresent(typeof(TreeViewItem))) e.Effects = e.AllowedEffects;
e.Handled = true;
}
private void textBox1_PreviewDrop(object sender, DragEventArgs e) {
var item = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem));
textBox1.Text = item.Header.ToString(); // Replace this with your own code
e.Handled = true;
}
private void textBox1_PreviewDragOver(object sender, DragEventArgs e) {
e.Handled = true;
}
Problem: In my WPF version, I am setting the textbox's text to empty (as a test), which occurs, but afterwards the TreeViewItem's header text is being pasted which is not what I want.
I think a parent UI element is handling (and therefore overriding) the Drop event so you're not getting the results you expect. As a matter of fact, when trying to recreate your issue, I couldn't even get my TextBox.Drop event to fire. However, using the TextBox's PreviewDrop event, I was able to get what (I think) is your expected result. Try this:
private void textBox1_PreviewDrop(object sender, DragEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb != null)
{
// If the DataObject contains string data, extract it.
if (e.Data.GetDataPresent(DataFormats.StringFormat))
{
string fileName = e.Data.GetData(DataFormats.StringFormat) as string;
using (StreamReader s = File.OpenText(fileName))
{
((TextBox)sender).Text = s.ReadToEnd();
}
}
}
e.Handled = true; //be sure to set this to true
}
I think that code snippet should answer most of the questions you posed except for this one:
Why is TreeViewItem node always null in the WPF version?
The DataObject you are passing in the DragDrop event does not support passing a TreeViewItem. In your code (and mine) we specify that the data format will be DataFormats.StringFormat which cannot be cast to a TreeViewItem.
GetFullPath seems to be outputting a wrong value. What you want to drag/drop is the Header and you can get it directly from item. Also bear in mind that the method below is associated with the MouseMove Event of the TreeView.
private void TreeView_MouseMove(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
TreeViewItem item = e.Source as TreeViewItem;
if (item != null)
{
DataObject dataObject = new DataObject();
dataObject.SetData(DataFormats.StringFormat, item.Header);
DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
}
}
I did create the drop part based on text rather than on the TreeViewItem (e.Data.GetData(typeof(string)).ToString()) but the most surprising thing is that it isn't even required. If you open a new C# WPF project, put a TreeView and a TextBox on it (, update the XAML part) and copy the code above, you can drop text from the TreeView into the TextBox without doing anything else!! The text is copied into the TextBox without accounting for the Drop handling.
Am I using the correct events?:
I think you are using the correct events, but I think you have several problems in your code.
I assume you have set the DataContext of your treeview to the real items and you use binding.
How do I access the DataObject in textbox_Drop ? -->
For getting the DataObject you have to get the real item by recursion (other solutions possible)
DependencyObject k = VisualTreeHelper.HitTest(tv_treeView, DagEventArgs.GetPosition(lv_treeView)).VisualHit;
while (k != null)
{
if (k is TreeViewItem)
{
TreeViewItem treeNode = k as TreeViewItem;
// Check if the context is your desired type
if (treeNode.DataContext is YourType)
{
// save the item
targetTreeViewItem = treeNode;
return;
}
}
else if (k == tv_treeview)
{
Console.WriteLine("Found treeview instance");
return;
}
// Get the parent item if no item from YourType was found
k = VisualTreeHelper.GetParent(k);
}
Why is the text being pasted in the WPF version? -->
The Header is displayed because (I assume) it is like the tostring method on your items. If for a complex item the binding is not specified, the ToString Method is executed.
Try not to set the Text directly in the handler of the drop event. Set the data context to your item (to the item you found in 1. point) and then specify the binding path via XAML. (for displaying)

Informing ViewModel of ValidatesOnExceptions input errors

In my application I have numerical (double or int) ViewModel properties that are bound to TextBoxes. The ViewModel implements IDataErrorInfo to check if the values entered fall within acceptable ranges for the 'business logic' (e.g. height can't be a negative value). I have a number of TextBoxes per page and have a button (think 'next' in a wizard) thats enabled property is bound to a ViewModel boolean that specifies whether there are any errors on the page as a whole. The enable/disable state of the button is properly updated with valid/invalid values according to the IDataErrorInfo rules I've written.
However, there is no way to let my viewmodel know when an exception has been thrown because an input value does not convert (i.e. "12bd39" is not a valid double) and as a result in the case of conversion exceptions my 'next' button will remain enabled despite bad input. The GUI however properly reflects the error with an adorner because of my binding:
<TextBox Text="{Binding Temperature, Mode=TwoWay, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
How can I let the view know that a 'ValidatesOnExceptions' style error has occured. Josh Smith's take here seems to rely on making every ViewModel property a string and rolling your own exception checking which seems like a lot of additional work. I additionally began looking into Karl Shifflett's implementation here, but I cannot seem to capture the routed event I would expect when putting this code into the view's codebehind file:
public ViewClass()
{
this.InitializeComponent();
this.AddHandler(System.Windows.Controls.Validation.ErrorEvent, new RoutedEventHandler(ValidationErrorHandler));
}
private void ValidationErrorHandler(object sender, RoutedEventArgs e)
{
var blah = e as System.Windows.Controls.ValidationErrorEventArgs;
if (blah.Action == ValidationErrorEventAction.Added)
{
}
else if (blah.Action == ValidationErrorEventAction.Removed)
{
}
}
Silverlight appears to have an event that you can subscribe too, but I cannot find the exact equivalent in WPF (3.5). Any help is appreciated!
I have a base class for the View that subscribes to Validation.ErrorEvent routed event
public class MVVMViewBase : UserControl
{
private RoutedEventHandler _errorEventRoutedEventHandler;
public MVVMViewBase()
{
Loaded += (s, e) =>
{
_errorEventRoutedEventHandler = new RoutedEventHandler(ExceptionValidationErrorHandler);
AddHandler(Validation.ErrorEvent, _errorEventRoutedEventHandler);
};
Unloaded += (s, e) =>
{
if (_errorEventRoutedEventHandler != null)
{
RemoveHandler(Validation.ErrorEvent, _errorEventRoutedEventHandler);
_errorEventRoutedEventHandler = null;
}
};
}
private void ExceptionValidationErrorHandler(object sender, RoutedEventArgs e)
{
ValidationErrorEventArgs args = (ValidationErrorEventArgs) e;
if (!(args.Error.RuleInError is IUiValidation)) return;
DataErrorInfoViewModelBase viewModelBase = DataContext as DataErrorInfoViewModelBase;
if(viewModelBase == null) return;
BindingExpression bindingExpression = (BindingExpression) args.Error.BindingInError;
string dataItemName = bindingExpression.DataItem.ToString();
string propertyName = bindingExpression.ParentBinding.Path.Path;
e.Handled = true;
if(args.Action == ValidationErrorEventAction.Removed)
{
viewModelBase.RemoveUIValidationError(new UiValidationError(dataItemName, propertyName, null));
return;
}
string validationErrorText = string.Empty;
foreach(ValidationError validationError in Validation.GetErrors((DependencyObject) args.OriginalSource))
{
if (validationError.RuleInError is IUiValidation)
{
validationErrorText = validationError.ErrorContent.ToString();
}
}
viewModelBase.AddUIValidationError(new UiValidationError(dataItemName, propertyName, validationErrorText));
}
}
and a base class for the ViewModel = DataErrorInfoViewModelBase that is informed by
AddUIValidationError and RemoveUIValidationError
Also all my ValidationRule classes implement IUiValidation which is used just to mark the class as taking part of the UI errors propagation(no members). (you can use an attribute for the same purpose).

Categories