Visual elements in Xamarin Forms are made visible/invisible by setting their IsVisible property. Changing some properties or data on visual elements can cause the interface to redraw, which can be expensive.
I'm trying to improve performance on my app so I've introduced caching and tried to make some layout simplifications, but I have to make a lot of visual elements visible/invisible and wondered wht the best way would be to optimise this.
Which is the more efficient operation?
var myButton.IsVisible = true;
Or:
if(!myButton.IsVisible) myButton.IsVisible = true;
If Xamarin already checks the state and decides whether to redraw then the second option is redundant and therefore inefficient. If a redraw happens every time the property is set then it will be more efficient. I can't find any documentation to tell me which is the case.
Any good implementation of a DependencyProperty (or INotifyPropertyChanged) member should ignore successive sets if that value is the same. For that reason let IsVisible work out what to do rather than place responsiblity on callers.
If Xamarin already checks the state and decides whether to redraw then the second option is redundant and therefore inefficien
That's right. Xamarin, like other XAML-based implemenations, are visually-efficient (unlike WinForms) so setting a visual property is not likely to immediately cause a screen refresh. Additonally, the entire app window is rendered in a single blit to avoid flicker.
In Xamarin, setting button.IsVisible for example makes it way down to BindableObject.SetValueActual (Button has BindableObject in its inheritance graph) where checks are made for sameness. The new value is only applied if the value is different or if SetValueFlags.RaiseOnEqual is set.
OnPropertyChanged and PropertyChanged are called on different values or if RaiseOnEqual is set.
Xamarin source code from GitHub: (my comments)
void SetValueActual(BindableProperty property, BindablePropertyContext context, object value, bool currentlyApplying, SetValueFlags attributes, bool silent = false)
{
object original = context.Value;
bool raiseOnEqual = (attributes & SetValueFlags.RaiseOnEqual) != 0;
bool clearDynamicResources = (attributes & SetValueFlags.ClearDynamicResource) != 0;
bool clearOneWayBindings = (attributes & SetValueFlags.ClearOneWayBindings) != 0;
bool clearTwoWayBindings = (attributes & SetValueFlags.ClearTwoWayBindings) != 0;
bool same = ReferenceEquals(context.Property, BindingContextProperty) ? ReferenceEquals(value, original) : Equals(value, original);
if (!silent && (!same || raiseOnEqual))
{
property.PropertyChanging?.Invoke(this, original, value);
OnPropertyChanging(property.PropertyName);
}
if (!same || raiseOnEqual)
{
context.Value = value; // <---------- assignment
}
.
.
.
if (!silent && (!same || raiseOnEqual)) // <------- notifications guard
{
if (binding != null && !currentlyApplying)
{
_applying = true;
binding.Apply(true);
_applying = false;
}
OnPropertyChanged(property.PropertyName);
property.PropertyChanged?.Invoke(this, original, value);
Related
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. :-)
Can somebody provide an overview for use of the XamlBindingHelper class with examples? Specifically the GetDataTemplateComponent and SetDataTemplateComponent method.
In the official document, it says
This class is for use in code that is generated by the XAML compiler.
This tells me that I should be able to find some reference of it in code-generated classes (.g.cs) by x:Bind, given there's not a single thread on the Internet that explains what exactly it does.
So I created a test UWP project with a ListView, and inside its ItemTemplate I threw in some x:Bind with x:Phase. After I compiled the project, I found some of its methods used inside my MainPage.g.cs -
XamlBindingHelper.ConvertValue
public static void Set_Windows_UI_Xaml_Controls_ItemsControl_ItemsSource(global::Windows.UI.Xaml.Controls.ItemsControl obj, global::System.Object value, string targetNullValue)
{
if (value == null && targetNullValue != null)
{
value = (global::System.Object) global::Windows.UI.Xaml.Markup.XamlBindingHelper.ConvertValue(typeof(global::System.Object), targetNullValue);
}
obj.ItemsSource = value;
}
Apparently the XamlBindingHelper.ConvertValue method is for converting values. I knew this already, as I used it in one of my recent answers on SO.
XamlBindingHelper.SuspendRendering & XamlBindingHelper.ResumeRendering
public int ProcessBindings(global::Windows.UI.Xaml.Controls.ContainerContentChangingEventArgs args)
{
int nextPhase = -1;
switch(args.Phase)
{
case 0:
nextPhase = 1;
this.SetDataRoot(args.Item);
if (!removedDataContextHandler)
{
removedDataContextHandler = true;
((global::Windows.UI.Xaml.Controls.StackPanel)args.ItemContainer.ContentTemplateRoot).DataContextChanged -= this.DataContextChangedHandler;
}
this.initialized = true;
break;
case 1:
global::Windows.UI.Xaml.Markup.XamlBindingHelper.ResumeRendering(this.obj4);
nextPhase = -1;
break;
}
this.Update_((global::System.String) args.Item, 1 << (int)args.Phase);
return nextPhase;
}
public void ResetTemplate()
{
this.bindingsTracking.ReleaseAllListeners();
global::Windows.UI.Xaml.Markup.XamlBindingHelper.SuspendRendering(this.obj4);
}
XamlBindingHelper.SuspendRendering & XamlBindingHelper.ResumeRendering look very interesting. They seem to be the key functions to enable ListView/GridView's incremental item rendering which helps improve the overall panning/scrolling experience.
So apart from x:DeferLoadingStrategy and x:Load(Creators Update), they are something else that could be used to improve your app performance.
IDataTemplateComponent & IDataTemplateExtension
However, I couldn't find anything related to GetDataTemplateComponent and SetDataTemplateComponent. I even tried to manually set this attached property in XAML but the get method always returned null.
And here's the interesting bit. I later found this piece of code in the generated class.
case 2: // MainPage.xaml line 13
{
global::Windows.UI.Xaml.Controls.Grid element2 = (global::Windows.UI.Xaml.Controls.Grid)target;
MainPage_obj2_Bindings bindings = new MainPage_obj2_Bindings();
returnValue = bindings;
bindings.SetDataRoot(element2.DataContext);
element2.DataContextChanged += bindings.DataContextChangedHandler;
global::Windows.UI.Xaml.DataTemplate.SetExtensionInstance(element2, bindings);
}
break;
The method DataTemplate.SetExtensionInstance looks very similar to XamlBindingHelper.SetDataTemplateComponent. It takes element2 which is the root Grid inside the ItemTemplate of my ListView, and an IDataTemplateExtension; where the latter takes an element and an IDataTemplateComponent. If you have a look at their definitions, their functionalities are very similar, which makes me think if DataTemplate.SetExtensionInstance is the replacement of XamlBindingHelper.SetDataTemplateComponent? I'd love to know if otherwise.
Unlike IDataTemplateComponent, you can get an instance of the IDataTemplateExtension in your code -
var firstItemContainer = (ListViewItem)MyListView.ContainerFromIndex(0);
var rootGrid = (Grid)firstItemContainer?.ContentTemplateRoot;
var dataTemplateEx = DataTemplate.GetExtensionInstance(rootGrid);
In my case, the dataTemplateEx is an instance of another generated class called MainPage_obj2_Bindings, where you have access to methods like ResetTemplate and ProcessBindings.
I assume they could be helpful if you were to build your own custom list controls, but other than that I just can't see why you would ever need them.
I am auto-generating code that creates a winform dialog based on configuration (textboxes, dateTimePickers etc). The controls on these dialogs are populated from a saved dataset and
needs to Set and Get properties for various control objects (custom or other).
//Upon opening of form - populate control properties with saved values
MyObject.Value = DataSource.GetValue("Value");
//Upon closing of form, save values of control properties to dataset.
DataSource.SetValue("Value") = MyObject.Value;
Now this is all fine, but what of readOnly properties? I wish to save the result of the property but need to know when to NOT generate code that will attempt to populate it.
//Open form, attempt to populate control properties.
//Code that will result in 'cannot be assigned to -- it is read only'
MyObject.HasValue = DataSource.GetValue("HasValue");
MyObject.DerivedValue = DataSource.GetValue("Total_SC2_1v1_Wins");
//Closing of form, save values.
DataSource.SetValue("HasValue") = MyObject.HasValue;
Remember that I do not know the type of object I've instantiate until runtime.
How can I (at runtime) identify a readonly property?
With PropertyDescriptor, check IsReadOnly.
With PropertyInfo, check CanWrite (and CanRead, for that matter).
You may also want to check [ReadOnly(true)] in the case of PropertyInfo (but this is already handled with PropertyDescriptor):
ReadOnlyAttribute attrib = Attribute.GetCustomAttribute(prop,
typeof(ReadOnlyAttribute)) as ReadOnlyAttribute;
bool ro = !prop.CanWrite || (attrib != null && attrib.IsReadOnly);
IMO, PropertyDescriptor is a better model to use here; it will allow custom models.
I noticed that when using PropertyInfo, the CanWrite property is true even if the setter is private. This simple check worked for me:
bool IsReadOnly = prop.SetMethod == null || !prop.SetMethod.IsPublic;
Also - See Microsoft Page
using System.ComponentModel;
// Get the attributes for the property.
AttributeCollection attributes =
TypeDescriptor.GetProperties(this)["MyProperty"].Attributes;
// Check to see whether the value of the ReadOnlyAttribute is Yes.
if(attributes[typeof(ReadOnlyAttribute)].Equals(ReadOnlyAttribute.Yes)) {
// Insert code here.
}
I needed to use this for different classes, so I created this generic function:
public static bool IsPropertyReadOnly<T>(string PropertyName)
{
MemberInfo info = typeof(T).GetMember(PropertyName)[0];
ReadOnlyAttribute attribute = Attribute.GetCustomAttribute(info, typeof(ReadOnlyAttribute)) as ReadOnlyAttribute;
return (attribute != null && attribute.IsReadOnly);
}
When I set SelectedNode to null the tree updates correctly but BeforeSelect and AfterSelect do not fire.
Is there any way to tell when the selection has been changed to null?
My first thought is to extend the control and add an event though I would have thought something like this would already be available.
I think your solution is good. However, I just discovered this control (keep an eye on SO's right column :)):
http://treeviewadv.sourceforge.net/
which supports what you're looking for, maybe has other goodies too...
It seems the only way I could do this was to create a new control and provide a new implementation for SelectedNode, even OnAfterSelect and OnBeforeSelect weren't getting called.
public new TreeNode SelectedNode {
get { return base.SelectedNode; }
set {
// Remember, if `value' is not null this will be called in `base'.
if (value == null) {
TreeViewCancelEventArgs args
= new TreeViewCancelEventArgs(value, false, TreeViewAction.Unknown);
OnBeforeSelect(args);
if (args.Cancel)
return;
}
base.SelectedNode = value;
// Remember, if `value' is not null this will be called in `base'.
if (value == null) {
OnAfterSelect(new TreeViewEventArgs(value, TreeViewAction.Unknown));
}
}
}
Is there any way to force a listview control to treat all clicks as though they were done through the Control key?
I need to replicate the functionality of using the control key (selecting an item sets and unsets its selection status) in order to allow the user to easily select multiple items at the same time.
Thank you in advance.
It's not the standard behaviour of the ListView control, even when MultiSelect is set to true.
If you wanted to create your own custom control you would need to do the following:
Derive a control from ListView
add a handler to the "Selected" event.
In the "OnSelected", maintain your own list of selected items.
If the newly selected item is not in your list, add it. If it is, remove it.
In code, select all of the items in your list.
Should be simple enough to implement and feel like multi-select without using the control key!
Here is a complete solution that is a modification of the solution provided by Matthew M. above.
It offers an improvement as well as a bit of added functionality.
Improvement:
left clicking the control gives focus to the control.
right mouse click behaviour is consistent (single selection)
Added functionality:
the control has a property (MultiSelectionLimit) that allows you to put a limit on how many items can be selected at once.
After my first posting I realised a minor problem with the code. Clearing multiple selections would lead to the ItemSelectionChanged event being invoked multiple times.
I could find no way to avoid this with the current inheritance, so instead I adopted a solution where the bool property SelectionsBeingCleared will be true while until all selected items have been deselected.
This way a simple call to that property will make it possible to avoid updating effects until all of the multiple selections have been cleared.
public class ListViewMultiSelect : ListView
{
public const int WM_LBUTTONDOWN = 0x0201;
public const int WM_RBUTTONDOWN = 0x0204;
private bool _selectionsBeingCleared;
/// <summary>
/// Returns a boolean indicating if multiple items are being deselected.
/// </summary>
/// <remarks> This value can be used to avoid updating through events before all deselections have been carried out.</remarks>
public bool SelectionsBeingCleared
{
get
{
return this._selectionsBeingCleared;
}
private set
{
this._selectionsBeingCleared = value;
}
}
private int _multiSelectionLimit;
/// <summary>
/// The limit to how many items that can be selected simultaneously. Set value to zero for unlimited selections.
/// </summary>
public int MultiSelectionLimit
{
get
{
return this._multiSelectionLimit;
}
set
{
this._multiSelectionLimit = Math.Max(value, 0);
}
}
public ListViewMultiSelect()
{
this.ItemSelectionChanged += this.multiSelectionListView_ItemSelectionChanged;
}
public ListViewMultiSelect(int selectionsLimit)
: this()
{
this.MultiSelectionLimit = selectionsLimit;
}
private void multiSelectionListView_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
if (e.IsSelected)
{
if (this.MultiSelectionLimit > 0 && this.SelectedItems.Count > this.MultiSelectionLimit)
{
this._selectionsBeingCleared = true;
List<ListViewItem> itemsToDeselect = this.SelectedItems.Cast<ListViewItem>().Except(new ListViewItem[] { e.Item }).ToList();
foreach (ListViewItem item in itemsToDeselect.Skip(1)) {
item.Selected = false;
}
this._selectionsBeingCleared = false;
itemsToDeselect[0].Selected = false;
}
}
}
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_LBUTTONDOWN:
if (this.SelectedItems.Count == 0 || !this.MultiSelect) { break; }
if (this.MultiSelectionLimit > 0 && this.SelectedItems.Count > this.MultiSelectionLimit) { this.ClearSelections(); }
int x = (m.LParam.ToInt32() & 0xffff);
int y = (m.LParam.ToInt32() >> 16) & 0xffff;
ListViewHitTestInfo hitTest = this.HitTest(x, y);
if (hitTest != null && hitTest.Item != null) { hitTest.Item.Selected = !hitTest.Item.Selected; }
this.Focus();
return;
case WM_RBUTTONDOWN:
if (this.SelectedItems.Count > 0) { this.ClearSelections(); }
break;
}
base.WndProc(ref m);
}
private void ClearSelections()
{
this._selectionsBeingCleared = true;
SelectedListViewItemCollection itemsToDeselect = this.SelectedItems;
foreach (ListViewItem item in itemsToDeselect.Cast<ListViewItem>().Skip(1)) {
item.Selected = false;
}
this._selectionsBeingCleared = false;
this.SelectedItems.Clear();
}
}
You might want to also consider using Checkboxes on the list view. It's an obvious way to communicate the multi-select concept to your average user who may not know about Ctrl+Click.
From the MSDN page:
The CheckBoxes property offers a way to select multiple items in the ListView control without using the CTRL key. Depending on your application, using check boxes to select items rather than the standard multiple selection method may be easier for the user. Even if the MultiSelect property of the ListView control is set to false, you can still display checkboxes and provide multiple selection capabilities to the user. This feature can be useful if you do not want multiple items to be selected yet still want to allow the user to choose multiple items from the list to perform an operation within your application.
Here is the complete solution that I used to solve this problem using WndProc. Basically, it does a hit test when the mouse is clicked.. then if MutliSelect is on, it will automatically toggle the item on/off [.Selected] and not worry about maintaining any other lists or messing with the ListView functionality.
I haven't tested this in all scenarios, ... it worked for me. YMMV.
public class MultiSelectNoCTRLKeyListView : ListView {
public MultiSelectNoCTRLKeyListView() {
}
public const int WM_LBUTTONDOWN = 0x0201;
protected override void WndProc(ref Message m) {
switch (m.Msg) {
case WM_LBUTTONDOWN:
if (!this.MultiSelect)
break;
int x = (m.LParam.ToInt32() & 0xffff);
int y = (m.LParam.ToInt32() >> 16) & 0xffff;
var hitTest = this.HitTest(x, y);
if (hitTest != null && hitTest.Item != null)
hitTest.Item.Selected = !hitTest.Item.Selected;
return;
}
base.WndProc(ref m);
}
}
Drill down through ListviewItemCollection and you can set the Selected property for individual items to true. This will, I believe, emulate the "multi-select" feature that you are trying to reproduce. (Also, as the above commenter mentioned, be sure to have the MultiSelect property of the lisetview set to true.)
Just in case anyone else has searched for and found this article, the accepted solution is no longer valid. (in fact I am not sure it ever was). In order to do what you want (select multiple without a modifier key) simply set the list view selection type to be multiple, rather than extended. Multiple selects one item after another when clicked, and extended requires the modifier key to be pressed first.
The Ctrl+Click behavior is as implemented by the browser, and has little to do with the actual .NET Control. The result you're trying to achieve can be acquired with a lot of additional JavaScript - the easiest way would probably be to build a JavaScript control from default that works this way, rather than trying to hack up the listview. Would this be desirable? In that case I could look into it and get back to you with a solution.