The content inside the child window changes, which causes my child window to lose it's center alignment.... After the content has changed is there anyway to reposition the child window to center... I tried the following and it did not work:
this.horizontalalignment = horizontalalignment.center;
this.verticalalignment = verticalalignment.center;
Thanks
The presence of a RenderTransform on the ChildWindow template seems to be to blame. The TransformGroup is part of the default template to allow you to move around the window.
Here is a hack to reset the transform after you change the layout:
//after you do some change to the childwindow layout
sp.Children.Add(new Button() { Content = "a" });
Dispatcher.BeginInvoke(() =>
{
//reset the transform to zero
(this.GetTemplateChild("ContentRoot") as Grid).RenderTransform = new TransformGroup()
{
Children = { new ScaleTransform(), new SkewTransform(), new RotateTransform(), new TranslateTransform() }
};
});
or more automatically:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var contentRoot = this.GetTemplateChild("ContentRoot") as FrameworkElement;
contentRoot.LayoutUpdated += contentRoot_LayoutUpdated;
}
void contentRoot_LayoutUpdated(object sender, EventArgs e)
{
var contentRoot = this.GetTemplateChild("ContentRoot") as FrameworkElement;
var tg = contentRoot.RenderTransform as TransformGroup;
var tts = tg.Children.OfType<TranslateTransform>();
foreach (var t in tts)
{
t.X = 0; t.Y = 0;
}
}
LayoutUpdated gets called often, so you may want to check if contentRoot.ActualWidth and ActualHeight changed to see if you really need to wipe out the transform.
Code
public partial class DialogOptions : ChildWindow
{
public DialogOptions()
{
InitializeComponent();
Loaded += (sender, args) =>
{
VerticalAlignment = VerticalAlignment.Top;
this.SetWindowPosition(new Point(0, 200));
};
}
}
And extention:
public static void SetWindowPosition(this ChildWindow childWindow, Point point)
{
var root = VisualTreeHelper.GetChild(childWindow, 0) as FrameworkElement;
if (root == null)
{
return;
}
var contentRoot = root.FindName("ContentRoot") as FrameworkElement;
if (contentRoot == null)
{
return;
}
var group = contentRoot.RenderTransform as TransformGroup;
if (group == null)
{
return;
}
TranslateTransform translateTransform = null;
foreach (var transform in group.Children.OfType<TranslateTransform>())
{
translateTransform = transform;
}
if (translateTransform == null)
{
return;
}
translateTransform.X = point.X;
translateTransform.Y = point.Y;
}
Related
Created a custom intellisense textbox (textbox with listbox a child).
As shown in below image, the listbox pops up when i enter a char which all works fine and good but when i am at the end of textbox the listbox is partially visible, is there anyway i can show the whole listbox content?
Tried this "Show control inside user control outside the boundaries of its parent
But when the popup window opens the text box looses focus and i cannot type anything further, my intellisense textbox keeps giving better results based on what they type but in this situation i am not able to type anymore.
FYI tried to add pParentControl.Focus() into show method defined in other article as shown below, missing something?
public void Show(Control pParentControl)
{
if (pParentControl == null) return;
// position the popup window
var loc = pParentControl.PointToScreen(new Point(0, pParentControl.Height));
pParentControl.Focus();
m_tsdd.Show(loc);
}
Here is the complete code
class TextBox_AutoComplete : TextBox
{
#region Class Members
List<string> dictionary;
ListBox listbox = new ListBox();
#endregion
private PopupHelper m_popup;
#region Extern functions
[DllImport("user32")]
private extern static int GetCaretPos(out Point p);
#endregion
#region Constructors
public TextBox_AutoComplete() : base()
{
this.Margin = new Padding(0, 0, 0, 0);
this.Multiline = true;
this.Dock = DockStyle.Fill;
this.KeyDown += Textbox_KeyDown;
this.KeyUp += Textbox_KeyUp;
listbox.Parent = this;
listbox.KeyUp += List_OnKeyUp;
listbox.Visible = false;
this.dictionary = new List<string>();
}
#endregion
#region Properties
public List<string> Dictionary
{
get { return this.dictionary; }
set { this.dictionary = value; }
}
#endregion
#region Methods
private static string GetLastString(string s)
{
Regex rgx = new Regex("[^a-zA-Z0-9_.\\[\\]]");
s = rgx.Replace(s, " ");
string[] strArray = s.Split(' ');
return strArray[strArray.Length - 1];
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
Point cp;
GetCaretPos(out cp);
List<string> lstTemp = new List<string>();
List<string> TempFilteredList = new List<string>();
string LastString = GetLastString(this.Text.Substring(0, SelectionStart));
//MessageBox.Show(LastString);
/*seperated them so that column name matches are found first*/
TempFilteredList.AddRange(dictionary.Where(n => n.Replace("[", "").ToUpper().Substring(n.IndexOf(".") > 0 ? n.IndexOf(".") : 0).StartsWith(LastString.ToUpper())
).Select(r => r)
.ToList());
TempFilteredList.AddRange(dictionary.Where(n => n.Replace("[", "").ToUpper().StartsWith(LastString.ToUpper())
|| n.ToUpper().StartsWith(LastString.ToUpper()))
.Select(r => r)
.ToList());
lstTemp = TempFilteredList.Distinct().Select(r => r).ToList();
/*Getting max width*/
int maxWidth = 0, temp = 0;
foreach (var obj in lstTemp)
{
temp = TextRenderer.MeasureText(obj.ToString(), new Font("Arial", 10, FontStyle.Regular)).Width;
if (temp > maxWidth)
{
maxWidth = temp;
}
}
listbox.SetBounds(cp.X + 20, cp.Y + 20, maxWidth, 60);
if (lstTemp.Count != 0 && LastString != "")
{
listbox.DataSource = lstTemp;
// listbox.Show();
if (m_popup == null)
m_popup = new PopupHelper(listbox);
m_popup.Show(this);
}
else if (m_popup != null)
{
//listbox.Hide();
m_popup.Hide();
}
}
protected void Textbox_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Down)
{
if (listbox.Visible == true)
{
listbox.Focus();
}
e.Handled = true;
}
else if (e.KeyCode == Keys.Escape)
{
listbox.Visible = false;
e.Handled = true;
}
}
protected void Textbox_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space && listbox.Visible == true)
{
listbox.Focus();
List_OnKeyUp(listbox, new KeyEventArgs(Keys.Space));
e.Handled = true;
}
if (e.KeyCode == Keys.Down && listbox.Visible == true)
{
listbox.Focus();
e.Handled = true;
}
}
private void List_OnKeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)
{
int Selection_Start = this.SelectionStart;
string StrLS = GetLastString(this.Text.Substring(0, Selection_Start));
this.Select(Selection_Start - StrLS.Length, StrLS.Length);
// MessageBox.Show(this.Selection_Start.ToString() + " Last string" + StrLS);
this.SelectedText=((ListBox)sender).SelectedItem.ToString();
listbox.Hide();
this.Focus();
}
}
#endregion
}
public sealed class PopupHelper : IDisposable
{
private readonly Control m_control;
private readonly ToolStripDropDown m_tsdd;
private readonly Panel m_hostPanel; // workarround - some controls don't display correctly if they are hosted directly in ToolStripControlHost
public PopupHelper(Control pControl)
{
m_hostPanel = new Panel();
m_hostPanel.Padding = Padding.Empty;
m_hostPanel.Margin = Padding.Empty;
m_hostPanel.TabStop = false;
m_hostPanel.BorderStyle = BorderStyle.None;
m_hostPanel.BackColor = Color.Transparent;
m_tsdd = new ToolStripDropDown();
m_tsdd.CausesValidation = false;
m_tsdd.Padding = Padding.Empty;
m_tsdd.Margin = Padding.Empty;
m_tsdd.Opacity = 0.9;
m_control = pControl;
m_control.CausesValidation = false;
m_control.Resize += MControlResize;
//m_hostPanel.Controls.Add(m_control);
m_tsdd.Padding = Padding.Empty;
m_tsdd.Margin = Padding.Empty;
m_tsdd.MinimumSize = m_tsdd.MaximumSize = m_tsdd.Size = pControl.Size;
m_tsdd.Items.Add(new ToolStripControlHost(m_control));
}
private void ResizeWindow()
{
m_tsdd.MinimumSize = m_tsdd.MaximumSize = m_tsdd.Size = m_control.Size;
m_hostPanel.MinimumSize = m_hostPanel.MaximumSize = m_hostPanel.Size = m_control.Size;
}
private void MControlResize(object sender, EventArgs e)
{
ResizeWindow();
}
/// <summary>
/// Display the popup and keep the focus
/// </summary>
/// <param name="pParentControl"></param>
public void Show(Control pParentControl)
{
if (pParentControl == null) return;
// position the popup window
var loc = pParentControl.PointToScreen(new Point(0, pParentControl.Height));
pParentControl.Focus();
m_tsdd.Show(loc);
}
public void Hide()
{
m_tsdd.Hide();
}
public void Close()
{
m_tsdd.Close();
}
public void Dispose()
{
m_control.Resize -= MControlResize;
m_tsdd.Dispose();
m_hostPanel.Dispose();
}
}
Firstly, I personally don't see any benefit in having a control inside another. Yes, the child control is locked inside its parent's boundaries automatically for you, but this benefit is negated by the issue that you're facing, and solving that issue requires the same work as when the two controls have no relation. In both cases, you'll have to do the calculations manually to keep the child visible inside its parent. In the second case the parent is the app's window.
Secondly, I don't recommend using hacks like the one mentioned in the comments to show the child outside its parent's boundaries. The hack creates more issues than it solves, as you found out. And what's the point of that hack anyway? If you want to show the child outside the parent, then don't make it a child control in the first place, and you don't need any hack.
The best solution is the one that you find in any well designed app, and in Windows itself. Open any app, let's say Notepad, and right-click near the upper-left corner. You'll see the context menu pulling to lower-right direction. Now right-click near the other three corners and you'll see the context menu pulling in different direction each time, so it will always be visible inside the app. Now if you resize the app window too small and right-click, the context menu will choose the best direction but some of it will be outside the app because the window is too small. That's why you need your list not to be a child, but it's up to you, and it's only about these edge cases. The solution will be similar in both cases.
You're displaying the list in this line:
listbox.SetBounds(cp.X + 20, cp.Y + 20, maxWidth, 60);
The key is cp.X and cp.Y. This is what decides where the list will appear. You need to make this point dynamic and responsive to the boundaries of the parent. You fixed the width to maxWidth and height to 60, so I will use those values in the calculation.
To make sure the list will not go beyond the bottom:
var y = this.Height < cp.Y + 60 ? this.Height - 60 : cp.Y;
To make sure the list will not go beyond the right:
var x = this.Width < cp.X + maxWidth ? this.Width - maxWidth : cp.X;
Now you can show your list at the calculated point:
listbox.SetBounds(x, y, maxWidth, 60);
Notes:
I didn't include the 20 gap that you used. I think it looks better without the gap and I haven't seen any app that has a gap. If you prefer the gap, add it to the calculation of x and y. Don't add it in the SetBounds() or that will screw up the calculation.
The calculation above doesn't take into account when the parent size is too small to show the child inside. If you want to support that edge case, you need to make the child a separate control and add some checks to the calculation.
In my application I have the following situation:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:conv="clr-namespace:NumericTextBoxTest.Converters;assembly=NumericTextBoxTest"
xmlns:numericTextBox="clr-namespace:Syncfusion.SfNumericTextBox.XForms;assembly=Syncfusion.SfNumericTextBox.XForms"
x:Class="NumericTextBoxTest.MainPage">
<ScrollView>
<StackLayout>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
<Entry/>
</StackLayout>
</ScrollView>
</ContentPage>
Now If I click the blank space at the bottom (below the entries) i.e. the ScrollView the first Entry in the ScrollView will gain focus.
Very annoying If I am changing the value on the first Entry and trying to unfocus that Entry to set the value.
Is it possible to stop this behavior?
Now If I click the blank space at the bottom (below the entries) i.e. the ScrollView the first Entry in the ScrollView will gain focus.
In UWP it is by design that when the StackLayout gets tapped the system will search element for-each in the StackLayout until the first one which can be focused on. As a workaround to solve this issue, you can place an invisible buton in the top of StackLayout.
<ScrollView>
<StackLayout>
<Button HeightRequest="0" WidthRequest="1" />
<Entry />
....
<Entry />
</StackLayout>
</ScrollView>
The button will be focused on when StackLayout was tapped. The Entry will not be focused
In the end I actually ended up overriding the default ScrollViewRenderer as mentioned in this comment:
https://github.com/microsoft/microsoft-ui-xaml/issues/597#issuecomment-513804526
My ScrollViewRenderer on UWP looked like this:
using Xamarin.Forms;
using Xamarin.Forms.Platform.UWP;
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using ScrollBarVisibility = Xamarin.Forms.ScrollBarVisibility;
using UwpScrollBarVisibility = Windows.UI.Xaml.Controls.ScrollBarVisibility;
using Size = Xamarin.Forms.Size;
using Point = Xamarin.Forms.Point;
using Thickness = Xamarin.Forms.Thickness;
using FieldStrikeMove.Forms.CustomControls;
//https://github.com/microsoft/microsoft-ui-xaml/issues/597
//https://github.com/xamarin/Xamarin.Forms/blob/f17fac7b9e2225b1bfe9e94909d2b954106f8f1f/Xamarin.Forms.Platform.UAP/ScrollViewRenderer.cs
//07/01/20
[assembly: ExportRenderer(typeof(ExtendedScrollView), typeof(MyApp.UWP.CustomRenderers.Controls.ScrollViewRenderer))]
[assembly: ExportRenderer(typeof(ScrollView), typeof(MyApp.UWP.CustomRenderers.Controls.ScrollViewRenderer))]
namespace MApp.UWP.CustomRenderers.Controls
{
public class ScrollViewRenderer : ViewRenderer<ScrollView, ScrollViewer>//, IDontGetFocus
{
VisualElement _currentView;
bool _checkedForRtlScroll = false;
public ScrollViewRenderer()
{
AutoPackage = false;
}
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
{
SizeRequest result = base.GetDesiredSize(widthConstraint, heightConstraint);
result.Minimum = new Size(40, 40);
return result;
}
protected override Windows.Foundation.Size ArrangeOverride(Windows.Foundation.Size finalSize)
{
if (Element == null)
return finalSize;
Element.IsInNativeLayout = true;
Control?.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
Element.IsInNativeLayout = false;
return finalSize;
}
protected override void Dispose(bool disposing)
{
CleanUp(Element, Control);
base.Dispose(disposing);
}
protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize)
{
if (Element == null)
return new Windows.Foundation.Size(0, 0);
double width = Math.Max(0, Element.Width);
double height = Math.Max(0, Element.Height);
var result = new Windows.Foundation.Size(width, height);
Control?.Measure(result);
return result;
}
void CleanUp(ScrollView scrollView, ScrollViewer scrollViewer)
{
if (Element != null)
Element.PropertyChanged -= OnContentElementPropertyChanged;
if (ContainerElement != null)
ContainerElement.LayoutUpdated -= SetInitialRtlPosition;
if (scrollView != null)
{
scrollView.ScrollToRequested -= OnScrollToRequested;
}
if (scrollViewer != null)
{
scrollViewer.ViewChanged -= OnViewChanged;
if (scrollViewer.Content is FrameworkElement element)
{
element.LayoutUpdated -= SetInitialRtlPosition;
}
}
if (_currentView != null)
_currentView.Cleanup();
}
protected override void OnElementChanged(ElementChangedEventArgs<ScrollView> e)
{
base.OnElementChanged(e);
CleanUp(e.OldElement, Control);
if (e.NewElement != null)
{
if (Control == null)
{
SetNativeControl(new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibilityToUwp(e.NewElement.HorizontalScrollBarVisibility),
VerticalScrollBarVisibility = ScrollBarVisibilityToUwp(e.NewElement.VerticalScrollBarVisibility),
});
Control.ViewChanged += OnViewChanged;
}
Element.ScrollToRequested += OnScrollToRequested;
UpdateOrientation();
UpdateContent();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Content")
UpdateContent();
else if (e.PropertyName == Layout.PaddingProperty.PropertyName)
UpdateContentMargins();
else if (e.PropertyName == ScrollView.OrientationProperty.PropertyName)
UpdateOrientation();
else if (e.PropertyName == ScrollView.VerticalScrollBarVisibilityProperty.PropertyName)
UpdateVerticalScrollBarVisibility();
else if (e.PropertyName == ScrollView.HorizontalScrollBarVisibilityProperty.PropertyName)
UpdateHorizontalScrollBarVisibility();
}
protected void OnContentElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == View.MarginProperty.PropertyName)
UpdateContentMargins();
}
void UpdateContent()
{
if (_currentView != null)
_currentView.Cleanup();
if (Control?.Content is FrameworkElement oldElement)
{
oldElement.LayoutUpdated -= SetInitialRtlPosition;
if (oldElement is IVisualElementRenderer oldRenderer
&& oldRenderer.Element is View oldContentView)
oldContentView.PropertyChanged -= OnContentElementPropertyChanged;
}
_currentView = Element.Content;
IVisualElementRenderer renderer = null;
if (_currentView != null)
renderer = _currentView.GetOrCreateRenderer();
Control.Content = renderer != null ? renderer.ContainerElement : null;
UpdateContentMargins();
if (renderer?.Element != null)
renderer.Element.PropertyChanged += OnContentElementPropertyChanged;
if (renderer?.ContainerElement != null)
renderer.ContainerElement.LayoutUpdated += SetInitialRtlPosition;
}
async void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
{
ClearRtlScrollCheck();
// Adding items into the view while scrolling to the end can cause it to fail, as
// the items have not actually been laid out and return incorrect scroll position
// values. The ScrollViewRenderer for Android does something similar by waiting up
// to 10ms for layout to occur.
int cycle = 0;
while (Element != null && !Element.IsInNativeLayout)
{
await Task.Delay(TimeSpan.FromMilliseconds(1));
cycle++;
if (cycle >= 10)
break;
}
if (Element == null)
return;
double x = e.ScrollX, y = e.ScrollY;
ScrollToMode mode = e.Mode;
if (mode == ScrollToMode.Element)
{
Point pos = Element.GetScrollPositionForElement((VisualElement)e.Element, e.Position);
x = pos.X;
y = pos.Y;
mode = ScrollToMode.Position;
}
if (mode == ScrollToMode.Position)
{
Control.ChangeView(x, y, null, !e.ShouldAnimate);
}
Element.SendScrollFinished();
}
void SetInitialRtlPosition(object sender, object e)
{
if (Control == null) return;
if (Control.ActualWidth <= 0 || _checkedForRtlScroll || Control.Content == null)
return;
if (Element is IVisualElementController controller && controller.EffectiveFlowDirection.IsLeftToRight())
{
ClearRtlScrollCheck();
return;
}
var element = (Control.Content as FrameworkElement);
if (element.ActualWidth == Control.ActualWidth)
return;
ClearRtlScrollCheck();
Control.ChangeView(element.ActualWidth, 0, null, true);
}
void ClearRtlScrollCheck()
{
_checkedForRtlScroll = true;
if (Control.Content is FrameworkElement element)
element.LayoutUpdated -= SetInitialRtlPosition;
}
void OnViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
ClearRtlScrollCheck();
Element.SetScrolledPosition(Control.HorizontalOffset, Control.VerticalOffset);
if (!e.IsIntermediate)
Element.SendScrollFinished();
}
Windows.UI.Xaml.Thickness AddMargin(Thickness original, double left, double top, double right, double bottom)
{
return new Windows.UI.Xaml.Thickness(original.Left + left, original.Top + top, original.Right + right, original.Bottom + bottom);
}
// UAP ScrollView forces Content origin to be the same as the ScrollView origin.
// This prevents Forms layout from emulating Padding and Margin by offsetting the origin.
// So we must actually set the UAP Margin property instead of emulating it with an origin offset.
// Not only that, but in UAP Padding and Margin are aliases with
// the former living on the parent and the latter on the child.
// So that's why the UAP Margin is set to the sum of the Forms Padding and Forms Margin.
void UpdateContentMargins()
{
if (!(Control.Content is FrameworkElement element
&& element is IVisualElementRenderer renderer
&& renderer.Element is View contentView))
return;
var margin = contentView.Margin;
var padding = Element.Padding;
switch (Element.Orientation)
{
case ScrollOrientation.Horizontal:
// need to add left/right margins
element.Margin = AddMargin(margin, padding.Left, 0, padding.Right, 0);
break;
case ScrollOrientation.Vertical:
// need to add top/bottom margins
element.Margin = AddMargin(margin, 0, padding.Top, 0, padding.Bottom);
break;
case ScrollOrientation.Both:
// need to add all margins
element.Margin = AddMargin(margin, padding.Left, padding.Top, padding.Right, padding.Bottom);
break;
}
}
void UpdateOrientation()
{
//Only update the horizontal scroll bar visibility if the user has not set a desired state.
if (Element.HorizontalScrollBarVisibility != ScrollBarVisibility.Default)
return;
var orientation = Element.Orientation;
if (orientation == ScrollOrientation.Horizontal || orientation == ScrollOrientation.Both)
{
Control.HorizontalScrollBarVisibility = UwpScrollBarVisibility.Auto;
}
else
{
Control.HorizontalScrollBarVisibility = UwpScrollBarVisibility.Disabled;
}
}
UwpScrollBarVisibility ScrollBarVisibilityToUwp(ScrollBarVisibility visibility)
{
switch (visibility)
{
case ScrollBarVisibility.Always:
return UwpScrollBarVisibility.Visible;
case ScrollBarVisibility.Default:
return UwpScrollBarVisibility.Auto;
case ScrollBarVisibility.Never:
return UwpScrollBarVisibility.Hidden;
default:
return UwpScrollBarVisibility.Auto;
}
}
void UpdateVerticalScrollBarVisibility()
{
Control.VerticalScrollBarVisibility = ScrollBarVisibilityToUwp(Element.VerticalScrollBarVisibility);
}
void UpdateHorizontalScrollBarVisibility()
{
var horizontalVisibility = Element.HorizontalScrollBarVisibility;
if (horizontalVisibility == ScrollBarVisibility.Default)
{
UpdateOrientation();
return;
}
var orientation = Element.Orientation;
if (orientation == ScrollOrientation.Horizontal || orientation == ScrollOrientation.Both)
Control.HorizontalScrollBarVisibility = ScrollBarVisibilityToUwp(horizontalVisibility);
}
}
public static class Extensions
{
internal static void Cleanup(this VisualElement self)
{
if (self == null)
throw new ArgumentNullException("self");
IVisualElementRenderer renderer = Platform.GetRenderer(self);
foreach (Element element in self.Descendants())
{
var visual = element as VisualElement;
if (visual == null)
continue;
IVisualElementRenderer childRenderer = Platform.GetRenderer(visual);
if (childRenderer != null)
{
childRenderer.Dispose();
Platform.SetRenderer(visual, null);
}
}
if (renderer != null)
{
renderer.Dispose();
Platform.SetRenderer(self, null);
}
}
}
}
If I click the blank space at the bottom (below the entries)
you can stretch your stacklayout and then it will receive focus instead scrollview.
And you can also set backgroundcolor for stacklayout to make sure that stacklayout is stretched
[Update]
I just found out that I really needed to fully qulify the Namespace of my custom ScrollViewRenderer for the Target Parameter of the ExportRenderer Atrribute when the Class has the same Name as the one from Xamarin Forms.
Now it's working as expected
[Original]
I tried implementing the CustomRenderer posted by User1 but it seems like it is not used in the UWP Project. The Constructor of the ScrollViewRenderer is never called although I have a ScrollView on the Page.
If I derive a CustomScrollView from ScrollView and let the Renderer be applied to CustomScrollView it is used and the workaround is working as expected but it seems I'm not able to override the Renderer for all ScrollViews.
Any hint why the renderer isn't used to replace the default builtin ScrollViewRenderer of XF? (btw. I'm using XF 5.0.0.1874)
I want to draw line numbers in the left of a ListBox, very similar to what AvalonEdit does with LineNumberMargin. When ShowLineNumbers is true, it creates LineNumberMargin like this.
Anyways, I took a look at how they do it, and understood, and now I'm trying to apply something similar but using a ListBox (and its items) as a source for the drawing.
My control works like this: I have a separated ItemsControl docked to the left of ListBox. Each ItemsControl's item is an UIElement. DesignerLineNumberMargin is one of the ItemsControl's item and when the ItemsSource of the ListBox is set, I attach the ListBox to the DesignerLineNumberMargin. When DesignerLineNumberMargin is rendered, I iterate over ListBox's items and draw the line numbers.
DesignerLineNumberMargin.cs
public interface IMetadataAware
{
void Attach(ItemsControl control);
void Detach(ItemsControl control);
}
public class DesignerLineNumberMargin : FrameworkElement, IMetadataAware
{
private ItemsControl control;
private Typeface typeface;
private double emSize;
private int maxLineNumberLength = 2;
static DesignerLineNumberMargin()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DesignerLineNumberMargin),
new FrameworkPropertyMetadata(typeof(DesignerLineNumberMargin)));
}
protected override Size MeasureOverride(Size availableSize)
{
typeface = CreateTypeface();
emSize = (double)GetValue(TextBlock.FontSizeProperty);
var text = CreateText(new string('9', maxLineNumberLength));
return new Size(text.Width, 0);
}
private FormattedText CreateText(string text)
{
return
new FormattedText(
text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
emSize,
(Brush)GetValue(Control.ForegroundProperty));
}
protected override void OnRender(DrawingContext drawingContext)
{
if (control == null)
return;
var renderSize = RenderSize;
var foreground = (Brush)GetValue(Control.ForegroundProperty);
for (int index = 0; index < control.Items.Count; index++)
{
var item = control.Items[index];
var container = (FrameworkElement)control.ItemContainerGenerator.ContainerFromItem(item);
var text = CreateText((index + 1).ToString(CultureInfo.CurrentCulture));
//var y = container.Height;
var y = RenderSize.Height / (double)control.Items.Count;
drawingContext.DrawText(text, new Point(renderSize.Width - text.Width, y + index));
}
}
private Typeface CreateTypeface()
{
var element = this;
return new Typeface(
(FontFamily)element.GetValue(TextBlock.FontFamilyProperty),
(FontStyle)element.GetValue(TextBlock.FontStyleProperty),
(FontWeight)element.GetValue(TextBlock.FontWeightProperty),
(FontStretch)element.GetValue(TextBlock.FontStretchProperty));
}
public void Attach(ItemsControl control)
{
this.control = control;
var descriptor = TypeDescriptor.GetProperties(control)["ItemsSource"];
descriptor.AddValueChanged(control, OnItemsSourceChanged);
}
private void OnItemsSourceChanged(object sender, EventArgs e)
{
if (this.control.ItemsSource is INotifyCollectionChanged)
(this.control.ItemsSource as INotifyCollectionChanged).CollectionChanged += CollectionChanged;
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
InvalidateVisual();
}
public void Detach(ItemsControl control)
{
if (this.control == control)
{
var descriptor = TypeDescriptor.GetProperties(control)["ItemsSource"];
descriptor.RemoveValueChanged(control, OnItemsSourceChanged);
if (this.control.ItemsSource is INotifyCollectionChanged)
(this.control.ItemsSource as INotifyCollectionChanged).CollectionChanged -= CollectionChanged;
this.control = null;
}
InvalidateVisual();
}
}
The problem for me is figuring out the y-coordinate. When OnRender is called I do not know the Height of the ListBoxItem: Height, ActualHeight, DesiredSize is always 0.
Any insights?
I guess that's because your MeasureOverride() returns a Size with only a Width, but Height set to zero.
Try change the return statement to:
return new Size(text.Width, text.Height);
Hiiho! I am developing a dashboard view using Xamarin.Forms.
Now I want to allow a user to drag and drop my icons to rearrange my Grid. But for some reason I get no visual representation of the drag event on UWP. The "DragStarted" event those fire tho. Here is my code
This is my custom renderer in UWP.
[assembly: ExportRenderer(typeof(DashboardIcon), typeof(DashboardIconRenderer))]
namespace Paraply.Droid.Custom_Renderers
{
class DashboardIconRenderer : ViewRenderer<DashboardIcon, Windows.UI.Xaml.FrameworkElement>
{
public DashboardIconRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<DashboardIcon> e)
{
base.OnElementChanged(e);
if (this == null)
return;
this.CanDrag = true;
this.DragStarting += OnDragStarting;
//this.Holding += HandleHoldEvent;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
this.CanDrag = true;
}
private void OnDragStarting(object sender, DragStartingEventArgs e)
{
//This fires
}
}
My contentpage in Xamarin.Forms, or its the funciton that fills my Grid
public void InflateServices(IList<InstalledServiceModel> services)
{
int count = 0;
serviceBoard.Children.Clear();
foreach (InstalledServiceModel service in services)
{
DashboardIcon icon = new DashboardIcon(service.Name, service.IconURL, service.Id);
var p = new TapGestureRecognizer();
p.CommandParameter = icon;
p.SetBinding(TapGestureRecognizer.CommandProperty, "IconClick");
icon.GestureRecognizers.Add(p);
icon.Holding += HandleHoldEvent;
serviceBoard.Children.Add(icon, count % 3, count / 3);
count++;
}
}
Now every DashboardIcon is a class that derives from Grid. It looks like this
public class DashboardIcon : Grid
{
//public event EventHandler<DashboardHoldingEventArgs> Holding;
//public void OnHolding(DashboardHoldingEventArgs e) { Holding(this, e); }
public DashboardIcon(string title, string url, int id = -1)
{
Title = title;
URL = url;
ServiceId = id;
/*
UI design
*/
HorizontalOptions = LayoutOptions.FillAndExpand;
VerticalOptions = LayoutOptions.FillAndExpand;
RowDefinitions.Add(new RowDefinition() { Height = new GridLength(0.6, GridUnitType.Star) });
RowDefinitions.Add(new RowDefinition() { Height = new GridLength(0.4, GridUnitType.Star) });
image = new Image()
{
Source = url,
HorizontalOptions = LayoutOptions.CenterAndExpand,
Aspect = Aspect.AspectFit
};
text = new Label()
{
Text = title,
HorizontalTextAlignment = TextAlignment.Center,
TextColor = Color.Black
};
Children.Add(image, 0, 0);
Children.Add(text, 0, 1);
}
Now my OnDragStarting function does fire just fine. But for some reason I get no visual representation of the view I'm dragging, I was expecting the view to follow my finger until I drop it?
In OnDragStarting you can set e.DragUI.SetContentFromBitmap() for example. There you can specify what you want to see while dragging (the image probably). Also here you could set this.Opacity = 0.2; so it gets transparent or invisible.
I'm trying to allow my users to swap two DevExpress chart controls (although I believe pretty much any control should work...), by dragging one over the top of the other. I have done this for my TabControl (to allow swapping/moving of tabs), but for some reason I appear to be missing something here which is stopping me doing the same with my ChartControl.
It "should" draw a grey-ish box over the chartcontrol and allow the user to drag it to wherever they like, but I just get a black circle with a stripe through it.
Here is the code I have written so far, hopefully one of you will be able to spot the mistake and I can stop pulling my hair out! :)
private void ChartControlMouseMove(object sender, MouseEventArgs e)
{
// Handle Mouse move only if left button is pressed.
if (e.Button == MouseButtons.Left)
{
var chartControl = (ChartControl)sender;
// If the mouse moves outside the rectangle, start the drag.
if (!rectDragBoxFromMouseDown.Equals(Rectangle.Empty)
& !rectDragBoxFromMouseDown.Contains(e.X, e.Y))
{
Invalidate();
DoDragDrop(chartControl, DragDropEffects.Move);
CalcRectDragBox(e.X, e.Y);
Invalidate();
}
}
}
private void ChartControlMouseDown(object sender, MouseEventArgs e)
{
CalcRectDragBox(e.X, e.Y);
}
private void CalcRectDragBox(int x, int y)
{
// Remember the point where the mouse down occurred. The DragSize indicates
// the size that the mouse can move before a drag event should be started.
var dragSize = SystemInformation.DragSize;
// Create a rectangle using the DragSize, with the mouse position being
// at the center of the rectangle.
rectDragBoxFromMouseDown = new Rectangle(
new Point(x - (dragSize.Width/2), y - (dragSize.Height/2)), dragSize);
}
private void ChartControlDragOver(object sender, DragEventArgs e)
{
var chartControl = (ChartControl)sender;
// get the control we are hovering over.
var hitInformation = chartControl.CalcHitInfo(chartControl.PointToClient(new Point(e.X, e.Y)));
if ((hitInformation != null))
{
//ChartHitInfo hoverTab = hitInformation;
if (e.Data.GetDataPresent(typeof(ChartControl)))
{
e.Effect = DragDropEffects.Move;
var dragTab = (ChartControl)e.Data.GetData(typeof(ChartControl));
if (dragTab != chartControl)
{
for (int i = 0; i < layoutControlGroupDashboard.Items.Count; i++)
{
var layoutControlItem = layoutControlGroupDashboard.Items[i] as LayoutControlItem;
if (layoutControlItem != null && layoutControlItem.Control == chartControl)
{
for (int j = 0; j < layoutControlGroupDashboard.Items.Count; j++)
{
var controlItem = layoutControlGroupDashboard.Items[j] as LayoutControlItem;
if (controlItem != null && controlItem.Control == dragTab)
{
if (!_ignoreNextDrag)
{
_ignoreNextDrag = true;
layoutControlGroupDashboard.BeginInit();
var layoutControlItemi = layoutControlGroupDashboard.Items[i] as LayoutControlItem;
if (layoutControlItemi != null)
{
Control tempControlI =
layoutControlItemi.Control;
var layoutControlItemj = layoutControlGroupDashboard.Items[j] as LayoutControlItem;
if (layoutControlItemj != null)
{
layoutControlItemi.BeginInit();
layoutControlItemj.BeginInit();
Control tempControlJ =
layoutControlItemj.Control;
layoutControlItemi.Control =
null;
layoutControlItemj.Control =
null;
layoutControlItemi.Control =
tempControlJ;
layoutControlItemj.Control =
tempControlI;
layoutControlItemi.EndInit();
layoutControlItemj.EndInit();
}
}
layoutControlGroupDashboard.EndInit();
break;
}
else
{
_ignoreNextDrag = false;
break;
}
}
}
}
}
}
}
}
else
{
e.Effect = DragDropEffects.None;
}
}
Again, the idea is to allow the user to swap the controls around just click click-dragging things around... Hopefully it's just something simple I'm missing, but I can't see it for the life of me!
Edit: This is something I tried (adding my chart to a panel first...)
Panel panel = new Panel();
panel.Name = Guid.NewGuid().ToString();
panel.Controls.Add(chartControl);
var dashboardItem = new LayoutControlItem(layoutControlDashboard, panel)
{
Padding = new DevExpress.XtraLayout.Utils.Padding(0),
Spacing = new DevExpress.XtraLayout.Utils.Padding(0),
SizeConstraintsType = SizeConstraintsType.Custom
};
Here is the modified ChartControlDragOver method which work in case the ChartControl is placed in the LayoutControl:
private void ChartControlDragOver(object sender, DragEventArgs e) {
var chartControl = (ChartControl)sender;
// get the control we are hovering over.
var hitInformation = chartControl.CalcHitInfo(chartControl.PointToClient(new Point(e.X, e.Y)));
if ((hitInformation != null)) {
//ChartHitInfo hoverTab = hitInformation;
if (e.Data.GetDataPresent(typeof(ChartControl))) {
e.Effect = DragDropEffects.Move;
var dragTab = (ChartControl)e.Data.GetData(typeof(ChartControl));
if (dragTab == chartControl) return;
InsertType insertType = InsertType.Left;
Point hitPoint = chartControl.Parent.PointToClient(new Point(e.X, e.Y));
if (dragTab.Bounds.Left < hitPoint.X && dragTab.Bounds.Right > hitPoint.X) {
if (dragTab.Bounds.Top > hitPoint.Y)
insertType = InsertType.Top;
else if (dragTab.Bounds.Bottom < hitPoint.Y)
insertType = InsertType.Bottom;
} else if (dragTab.Bounds.Right < hitPoint.X)
insertType = InsertType.Right;
else if (dragTab.Bounds.Left > hitPoint.X)
insertType = InsertType.Left;
LayoutControl layout = (LayoutControl)chartControl.Parent;
layout.GetItemByControl(dragTab).Move(layout.GetItemByControl(chartControl), insertType);
}
} else {
e.Effect = DragDropEffects.None;
}
}