I have a really big wpf application with many nested controls using caliburn micro and MVVM.
One control needs a check a condition before(while) the user leaves. If the check fails the focus is transfered back to the said control.
Using Finding ALL child controls WPF
I solved the problem with child control also firing the LostFocus event. And I used the Dispatcher to put the focus back to the control.
my code looks like this:
public void LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var parent = ParentFinder.TryFindParent<ProfileFunctionView>(e.NewFocus as FrameworkElement);
//only fire for outside controls, not child controls
if (parent == null)
{
if (!Apply())
{
var restoreFocus = (System.Threading.ThreadStart)delegate { SyntaxEditor.Focus(); };
Application.Current.Dispatcher.BeginInvoke(restoreFocus);
//stuff should happen here
}
}
}
The problem is that, if I click on a tab control somewhere above my control f.e.. The focus is set back correctly, but than the tab changes. I want to prevent the controls that are clicked outside of my control to react if the condition fails.
Is this possible? Is my approach correct?
Please excuse my confusing title, this problem is really hard to formulate or google. Any help is appreciated.
So, the first thing I would do is use the FocusManager to create a focus scope. Here is the MSDN Documentation.
https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.focusmanager?view=netcore-3.1
Setting a focus scope is essentially a container of elements within the scope. So, for your example I would do something like this.
ProfileFunctionView ()
{
// This will make child controls use this as their FocusScope.
FocusManager.SetIsFocusScope ( this );
}
Next, now that we have created a FocusScope it is easy to check if the element that has focus is in scope or not.
public void LostKeyboardFocus( object sender, KeyboardFocusChangedEventArgs e )
{
if ( sender is UIElement element ) {
var scope = FocusManager.GetFocusScope ( element );
if ( scope is Window ) {
// This is the default focus scope you have actually lost focus.
RestoreFocus();
}
else if ( scope is ProfileFunctionView view )
{
// Haven't lost focus do nothing
return;
}
else
{
// Handle case where focus does not belong to either.
}
}
}
Another method and probably the more appropriate method would be to set the parent elements Focusable property to False. This will prevent the lost focus events from firing in the first place.
Edit: Adding information based on your comment.
If the events are unwanted Disable all controls out side of FocusScope.
ProfileFunctionView ()
{
// This will make child controls use this as their FocusScope.
FocusManager.SetIsFocusScope ( this );
// Better to do this during the Loaded event.
FocusManagerUtility.DisableUIOutsideOfScope ( scope: this );
}
public static class FocusScopeUtility
{
public static void DisableUIOutsideScope ( scope )
{
// Psuedo function to get the top level UIElement in the tree.
var container = GetOwningWindowOrPage ( scope );
// Psuedo function to get traversal of tree
var traversal = GetVisualTreeTraversalAsEnumerable ();
foreach ( var node in traversal )
{
var element = node as UIElement;
if ( FocusScopeUtility.IsInScope ( scope, element )
{
// This line disables the control from receiving input.
element.Enabled = false;
}
}
}
}
Related
I want to enable/disable controls in a Windows Forms application according to the user privileges.
Initially I thought of writing a method in each form class that would check the user credentials and then enable/disable its controls. But then I realized I could (maybe) create a static class method which would take the form as a parameter and do the job.
So I started writing it, presuming that sometimes I would like to enable the controls of just one or two panels, instead of the whole form. So, I need the parameters to be:
a varying number of panels and/or
a form class.
My difficulties with this task is that I'm getting an error trying to make the panels argument varying, and I have no idea how to set a parameter that could take any form class. All my form classes obviously inherits from Form generic class, but I don't know how to apply this.
Here's what I got:
public static void Enable(TableLayoutPanel[] containers = null)
{
if (MyOF.isEnabled)
{
return;
}
else
{
try
{
foreach (TableLayoutPanel table in containers)
{
foreach (Control control in table.Controls)
{
control.Enabled = false;
}
}
}
catch (NullReferenceException)
{
}
}
}
If we remember that the Form class derives from Control (indirectly, by deriving from ContainerControl which derives from ScrollableControl, which derives from Control), and the Enabled property belongs to the Control class, we can write a method that will enable any control's children (including the Form or TableLayoutPanel controls), since the Controls collection also belongs to the Control class:
public static void EnableChildren(Control control, bool enabled = true)
{
foreach (Control child in control.Controls)
{
child.Enabled = enabled;
}
}
And then if we also want to be able to use this with a collection of controls (as in your example), we can write an overload that takes a collection:
public static void EnableChildren(IEnumerable<Control> controls = null,
bool enabled = true)
{
if (controls == null) return;
foreach (var control in controls)
{
EnableChildren(control, enabled);
}
}
Now we can use this with a Form or a collection of TableLayoutPanel controls (or any control that has controls in it's Controls collection).
Examples of usage:
var myForm = new Form1();
EnableChildren(this); // 'this' is the current form
EnableChildren(myForm); // a separate instance of a form control
EnableChildren(tableLayoutPanel1, false); // A single TableLayoutPanel control
var tableLayoutPanels = new [] {tableLayoutPanel1, tableLayoutPanel2, tableLayoutPanel3};
EnableChildren(tableLayoutPanels); // An array of tableLayoutPanel controls
One of the simple ways I can think about what you are trying to do, is this. Let me get away for a sec here. I worked on projects where all form controls were built from Metadata. And meta came with licensing info. So, when control was placed where it should, it also was disabled or set read-only based on Metadata but the whole feature would be hidden if licensing info was restricting access to it. Coming back to your approach, this is not a bad approach and I see that this is can be done. And it can be done in 2 ways, (quickly from my head).
Use user controls as surface for the components you want to enable/disable. Create an interface
public interface IDisableableControl // make your fine name, no methods needed - marker interface
. . . . .
public class MyFineUserControl : UserControl, IDisableableControl
And in your static method that you're going to write pass the form, and find all controls that implement this interface and work them the way you want.
2.
Similarly, you can use property Tag, which is available on each control. With that, you can actually set your complex security object that can come from DB-stored metadata and then you evaluate this object stored in Tag to apply your configuration
Your method needs to be recursive
internal static void SetAllControls(Control parent)
{
// Do something with control, for example parent.Enabled = false
if (parent is IDisableableControl)
{
// here you use your logic, evaluate your parent you're dialing with and
// enable/disable correspondingly
parent.Enabled = false;
return;
}
foreach(var c in parent.Controls)
SetAllControls(c);
}
In real life, your TOP parent will be a form and will not need to be disabled, but it's certain children will. In fact, most of the time, once you found a UserControl which implements IDisableableControl that should be end of line, means, you don't need to go into children controls as they all sit on this parent and all will be disabled
I manage to accomplish what I was trying to do with the code below, which is pretty much a blend of all the helpful answers I got:
public static void EnableContainer(params Control[] containers)
{
if(containers.Count() == 0) { return; }
if (MyOF.isEnabled)
{
return;
}
else
{
try
{
foreach (var container in containers)
{
foreach (Control control in container.Controls)
{
control.Enabled = false;
}
}
}
catch (NullReferenceException)
{
}
}
}
public static void EnableForm<form>(form f) where form : Form
{
if (MyOF.isEnabled)
{
return;
}
else
{
foreach(Control control in f.Controls)
{
control.Enabled = false;
}
}
}
The community is welcome to suggest improvements as I am far from being a professional programmer. Thanks everyone once again.
I have a WPF CheckBox inside a Popup, and I'm finding if it is inside the item template of a TreeView, then the CheckBox does not respond to user input. If it is outside of the TreeView, then there are no problems.
I have created a relatively minimal mock-up here:
https://github.com/logiclrd/TestControlsInPopupsNotWorking
Does anyone know why the CheckBox controls popped up from within the TreeView cannot be checked?
I think this is an oversight in the design of the TreeView. Take a look at this:
Note: Some code excerpts were tidied up to avoid wrapping.
// This method is called when MouseButonDown on TreeViewItem and also listen
// for handled events too. The purpose is to restore focus on TreeView when
// mouse is clicked and focus was outside the TreeView. Focus goes either to
// selected item (if any) or treeview itself
internal void HandleMouseButtonDown()
{
if (!this.IsKeyboardFocusWithin)
{
if (_selectedContainer != null)
{
if (!_selectedContainer.IsKeyboardFocused)
_selectedContainer.Focus();
}
else
{
// If we don't have a selection - just focus the TreeView
this.Focus();
}
}
}
This method is called from TreeViewItem.OnMouseButtonDown, which we can see is a class-level handler that's configured to receive handled events too:
EventManager.RegisterClassHandler(
typeof(TreeViewItem),
Mouse.MouseDownEvent,
new MouseButtonEventHandler(OnMouseButtonDown),
/* handledEventsToo: */ true);
I have verified with the debugger that Handled is set to true by the time the event makes it to the TreeViewItem.
When you press down on the left mouse button over the CheckBox, the CheckBox begins a speculative 'click' operation and marks the event as handled. Normally, an ancestor element wouldn't see a handled event bubble up, but in this case it explicitly asked for them.
The TreeView sees that this.IsKeyboardFocusWithin resolves to false because the focused element is in another visual tree (the popup). It then gives focus back to the TreeViewItem.
Now, if you look in ButtonBase:
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
if (ClickMode == ClickMode.Hover)
{
// Ignore when in hover-click mode.
return;
}
if (e.OriginalSource == this)
{
if (IsPressed)
{
SetIsPressed(false);
}
if (IsMouseCaptured)
ReleaseMouseCapture();
IsSpaceKeyDown = false;
}
}
We see that IsPressed is set to false when focus is lost. If we then go to OnMouseLeftButtonUp, we see this:
bool shouldClick = !IsSpaceKeyDown && IsPressed && ClickMode == ClickMode.Release;
With IsPressed now false, the click operation never completes, all because the TreeViewItem stole focus away from you when you tried to click the button.
As a work-around, I have had success so far with using the NuGet library Ryder (which looks like a freely-usable open-source (MIT license) version of Microsoft Detours) to intercept the HandleMouseButtonDown method in TreeView.
The Ryder library can be found in the NuGet library, and the code behind it can be found here:
https://github.com/6A/Ryder
Hooking the HandleMouseButtonDown method is pretty simple:
var realMethod = typeof(System.Windows.Controls.TreeView).GetMethod("HandleMouseButtonDown", BindingFlags.Instance | BindingFlags.NonPublic);
var replacementMethod = typeof(Program).GetMethod(nameof(TreeView_HandleMouseButtonDown_shim), BindingFlags.Static | BindingFlags.NonPublic);
Redirection.Redirect(realMethod, replacementMethod);
The shim that replaces the method can basically do what the real method does but with a fix that detects the cross-visual-tree focus situation:
static void TreeView_HandleMouseButtonDown_shim(TreeView #this)
{
// Fix as seen in: https://developercommunity.visualstudio.com/content/problem/190202/button-controls-hosted-in-popup-windows-do-not-wor.html
if (!#this.IsKeyboardFocusWithin)
{
// BEGIN NEW LINES OF CODE
var keyboardFocusedControl = Keyboard.FocusedElement;
var focusPathTrace = keyboardFocusedControl as DependencyObject;
while (focusPathTrace != null)
{
if (ReferenceEquals(#this, focusPathTrace))
return;
focusPathTrace = VisualTreeHelper.GetParent(focusPathTrace) ?? LogicalTreeHelper.GetParent(focusPathTrace);
}
// END NEW LINES OF CODE
var selectedContainer = (System.Windows.Controls.TreeViewItem)TreeView_selectedContainer_field.GetValue(#this);
if (selectedContainer != null)
{
if (!selectedContainer.IsKeyboardFocused)
selectedContainer.Focus();
}
else
{
// If we don't have a selection - just focus the treeview
#this.Focus();
}
}
}
Some reflection is needed since this interacts with a private field that is not otherwise exposed from the TreeView class, but as work-arounds go, this is a lot less invasive than what I tried at first, which was importing the entirety of the TreeView class (and related types) from Reference Source into my project in order to alter the one member. :-)
I have a TabControl in which I want to prevent adding existing TabPage (they are identified by a name) and instead set the SelectedTabPage to this precise tab.
I wish to know if there are an event that triggers right before a page is being added to the TabControl. If not, would using the event CollectionChanged of the TabPages (list) be a correct alternative ?
I believe the event you're looking for is the Control.ControlAdded event:
http://msdn.microsoft.com/en-us/library/system.windows.forms.control.controladded.aspx
If that also detects when things inside the tab pages themselves are added, you should be able to filter out everything but TabPage controls using the ControlEventArgs.Control property in your event handler.
To reject adding a control will be a little more complicated. Since this event seems to only be raised after the control gets added, you'll need to do something like this:
void onControlAdded(object sender, ControlEventArgs e) {
var tab = e as TabPage;
if (tab == null)
return;
this.myTabControlObject.TabPages.Remove(tab);
}
This should remove the tab, but it will likely slow the tab adding process considerably.
Try something like this, I am checking the TabControl page Collection for a page with the same name as the Page that is trying to be added, if it exists I am setting focus to the existing instance, otherwise adding the new page to the TabControl. See if something like this works for you.
private void button1_Click(object sender, EventArgs e)
{
TabPage tp = new TabPage();
tp.Name = tabPage1.Name;
var temp =tabControl1.Controls.Find(tp.Name,true);
if( temp.Length > 0)
{
tabControl1.SelectedTab = (TabPage) temp[0];
}
else
tabControl1.Controls.Add(tp);
}
Anything having to do with the ControlCollection will most likely be triggered after the control has been added.
From above link:
You can determine if a Control is a member of the collection by passing the control into the Contains method. To get the index value of the location of a Control in the collection, pass the control into the IndexOf method. The collection can be copied into an array by calling the CopyTo method.
If you want you could cleanup your code some by adding an ExtensionMethod to your TabControl Check for an existing page, set focus or add from there.
Example:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static bool AddPage(this TabControl tc, TabPage tp)
{
var matchedPages = tc.Controls.Find(tp.Name, false);
if ( matchedPages.Length > 0)
{
tc.SelectedTab = (TabPage)matchedPages[0];
return true;
}
else
{
tc.TabPages.Add(tp);
tc.SelectedTab = tp;
return false;
}
}
}
}
Usage:
tabControl1.AddPage(tp);
I'm good with Loading the control, using the LoadControl("~/vitrualPath"), so I have:
UserControl ctrl = (UserControl)LoadControl("~/controls/someControl.ascx");
this.Controls.Add(ctrl);
//plcCtrl.Controls.Add(ctrl);
The trouble is that I wish to then loop through all the controls in the usercontrol:
foreach (Label c in this.Controls.OfType<Label>())
{
// It's a label for an input
if (c.ID.Substring(0, 8) == "lblInput")
{
// Do some stuff with the control here
}
}
However, the added controls aren't part of this, but part of ctrl
Is there a way I can add the contents of the loaded control to this or a way to loop through both this and ctrl in one hit?
If you simply want to loop through both top-level labels and labels in ctrl, try this.Controls.Concat(ctrl.Controls).OfType<Label>() in your foreach loop.
You can also move your if into a LINQ Where call:
.Where(l => l.ID.Substring(0, 8) == "lblInput")
By using a recursive function you don't need to worry about controls within sub levels/ containers. Something like this should be OK (all you need to do is to pass the top level control along with the id substring that you are interested in). So if the conditions are met it will do whatever you have intended to do with the control and at any sub level.
public void ProcessControl(Control control, string ctrlName)
{
foreach (Label c in control.Controls.OfType<Label>())
{
// It's a label for an input
if (c.ID.Substring(0, 8) == ctrlName)
{
// Do some stuff with the control here
}
}
foreach (Control ctrl in control.Controls)
{
ProcessControl(ctrl, ctrlName);
}
}
You should write a recursive method that starts looping the controls in this.Controls and goes down the tree of controls. It will then also go in your user control and find your labels.
I don't think there is a way to loop through both like you want.
You can easily create a method that receives a Control as parameter and iterate though its controls. Something like this:
void Function(Control control)
{
foreach (Label c in control.Controls.OfType<Label>())
{
// It's a label for an input
if (c.ID.Substring(0, 8) == "lblInput")
{
// Do some stuff with the control here
}
}
}
You should be able to access the controls inside the user control by accessing the this.Controls[index].Controls I think, however it kind of depends what you are trying to achieve? Their might be a cleaner way of doing what you are trying to do?
I am building a canvas control. This root canvas has several overlapping children (canvas as well). This is done so each child can handle its own drawing and I can then compose the final result with any combination of children to get the desired behavior.
This is working very well as far as rendering is concerned. This does not work so well with mouse events however. The way mouse events works are as follow (using previewmousemove as an example):
1- If root canvas is under mouse, fire event
2- Check all children, if one is under mouse, fire event and stop
As such, only the first child I add will receive the mouse move event. The event is not propagated to all children because they overlap.
To overcome this, I attempted the following:
1- Override mouse events in the root canvas
2- For every event, find all children that want to handle the event using VisualTreeHelper.HitTest
3- For all children that returned a valid hit test result (ie: under mouse and willing to handle the event (IsHitTestVisible == true)), ???
This is where I am stuck, I somehow need to send the mouse event to all children, and stop the normal flow of the event to make sure the first child doesn't receive it twice (via handled = true in the event).
By using RaiseEvent with the same event passed on the children, things seem to work but somehow it raises the event on the parent (root canvas) as well. To bypass this I needed to create a copy of the event and set force set the source though it appears to be more of a hack than a solution. Is there a proper way to do what I am trying to do? Code example follows.
public class CustomCanvas : Canvas
{
private List<object> m_HitTestResults = new List<object>();
public new event MouseEventHandler MouseMove;
public CustomCanvas()
{
base.PreviewMouseMove += new MouseEventHandler(CustomCanvas_MouseMove);
}
private void CustomCanvas_MouseMove(object sender, MouseEventArgs e)
{
// Hack here, why is the event raised on the parent as well???
if (e.OriginalSource == this)
{
return;
}
Point pt = e.GetPosition((UIElement)sender);
m_HitTestResults.Clear();
VisualTreeHelper.HitTest(this,
new HitTestFilterCallback(OnHitTest),
new HitTestResultCallback(OnHitTest),
new PointHitTestParameters(pt));
MouseEventArgs tmpe = new MouseEventArgs(e.MouseDevice, e.Timestamp, e.StylusDevice);
tmpe.RoutedEvent = e.RoutedEvent;
tmpe.Source = this;
foreach (object hit in m_HitTestResults)
{
UIElement element = hit as UIElement;
if (element != null)
{
// This somehow raises the event on us as well as the element here, why???
element.RaiseEvent(tmpe);
}
}
var handlers = MouseMove;
if (handlers != null)
{
handlers(sender, e);
}
e.Handled = true;
}
private HitTestFilterBehavior OnHitTest(DependencyObject o)
{
UIElement element = o as UIElement;
if (element == this)
{
return HitTestFilterBehavior.ContinueSkipSelf;
}
else if (element != null && element.IsHitTestVisible && element != this)
{
return HitTestFilterBehavior.Continue;
}
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
private HitTestResultBehavior OnHitTest(HitTestResult result)
{
// Add the hit test result to the list that will be processed after the enumeration.
m_HitTestResults.Add(result.VisualHit);
// Set the behavior to return visuals at all z-order levels.
return HitTestResultBehavior.Continue;
}
I think you should use the preview events because those are RoutingStrategy.Tunnel from Window to the most high control in Z-Order, and normal events are RoutingStrategy.Bubble.
In this RoutedEvents there are a property Handle when it's true the system will stop to traverse the visual tree because someone used this event.
I found your code example interesting so I gave it a try... but I had to make a small modification for it to work properly in my stuff.
I had to change the 2nd "if" in the HitTestFilter method as follow:
if (element == null || element.IsHitTestVisible)
As you can see I removed the useless "element != this" at the end (you already tested that condition in the 1st "if") and I added "element == null" at the beginning.
Why? Because at some point during the filtering the parameter type was System.Windows.Media.ContainerVisual which doesn't inherit from UIElement and so element would be set to null and ContinueSkipSelfAndChildren would be returned. But I don't want to skip the children because my Canvas is contained inside its "Children" collection and UIElements I want to hittest with are contained in Canvas.
Just as #GuerreroTook said, you should solve this by using WPF's RoutedEvents (more information here.