Related
I'm trying to make what I think will be a nice effect for an app - a series of images (think wallpaper) will be constantly scrolling in the background during a view. I started prototyping this in Xamarin.Forms, creating a custom control. Planned on a diagonal translation but started with the most basic approach and still ran into some issues fairly quickly, namely that it is not entirely smooth as it gets a bit choppy here and there (even when using caching and just a 10kb image) and 2) if user executes an action that's more involved it may cause a lag and the images get rendered more closely together than they should be. Is there a way to fix up this approach so that it's as smooth as possible and doesn't interfere (or get interfered with) the other UI elements, or is there a far superior approach for something like this - anyone ever tackle this? Please let me know, thanks.
FlyingImageBackground.cs
public class FlyingImageBackground : ContentView
{
public static readonly BindableProperty FlyingImageProperty =
BindableProperty.Create(nameof(FlyingImage), typeof(ImageSource), typeof(FlyingImageBackground), default(ImageSource), BindingMode.TwoWay, propertyChanged: OnFlyingImageChanged);
public ImageSource FlyingImage
{
get => (ImageSource)GetValue(FlyingImageProperty);
set => SetValue(FlyingImageProperty, value);
}
private AbsoluteLayout canvas;
public FlyingImageBackground()
{
this.canvas = new AbsoluteLayout()
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
};
this.canvas.SizeChanged += Canvas_SizeChanged;
Content = this.canvas;
}
~FlyingImageBackground() => this.canvas.SizeChanged -= Canvas_SizeChanged;
private static void OnFlyingImageChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackground)bindable;
control.BringToLife();
}
private void BringToLife()
{
if (this.canvas.Width <= 0 || this.canvas.Height <= 0)
return;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
Device.BeginInvokeOnMainThread(async () =>
{
await SendImageWave();
});
return this.canvas.IsVisible;
});
}
private async Task SendImageWave()
{
var startingX = -100;
var endingX = this.canvas.Width;
if (endingX <= 0)
return;
endingX += 100;
var yPositions = Enumerable.Range(0, (int)this.canvas.Height).Where(x => x % 90 == 0).ToList();
var imgList = new List<CachedImage>();
foreach (var yPos in yPositions)
{
var img = new CachedImage
{
Source = FlyingImage,
HeightRequest = 50
};
imgList.Add(img);
this.canvas.Children.Add(img, new Point(startingX, yPos));
}
await Task.WhenAll(
imgList.Select(x => x.TranslateTo(endingX, 0, 10000)));
//.Concat(imgList.Select(x => x.TranslateTo(startingX, 0, uint.MinValue))));
imgList.ForEach(x =>
{
this.canvas.Children.Remove(x);
x = null;
});
imgList = null;
}
private void Canvas_SizeChanged(object sender, EventArgs e)
{
BringToLife();
}
}
Usage example:
Just put it into a Grid in a ContentPage along with the main content:
e.g.:
<ContentPage.Content>
<Grid>
<controls:FlyingImageBackground FlyingImage="fireTruck.png" />
<StackLayout HorizontalOptions="Center">
<Button
Text="I'm a button!" />
<Label
FontAttributes="Bold,Italic"
Text="You're a good man, old sport!!!"
TextDecorations="Underline" />
</StackLayout>
</Grid>
</ContentPage.Content>
Switched to SkiaSharp and much better results. The animation appears smooth and if the flow gets interrupted the images maintain their appropriate distance. Also realized in the first draft using the built-in Xamarin Animations that I screwed up the check for when to run it; .IsVisible prop will remain true even if the page isn't even on the screen anymore, so in this new version needed to bind to a property that tells me if the page is actually active or not (based on when it gets navigated to and navigated away from) and if not then stop the animation. This is still just handling the horizontal scrolling effect for now. Hope someone else finds it useful, and any other improvements would be welcome, please just comment/post an answer!
[DesignTimeVisible(true)]
public class FlyingImageBackgroundSkia : ContentView
{
public static readonly BindableProperty IsActiveProperty =
BindableProperty.Create(
nameof(IsActive),
typeof(bool),
typeof(FlyingImageBackground),
default(bool),
BindingMode.TwoWay,
propertyChanged: OnPageActivenessChanged);
private SKCanvasView canvasView;
private SKBitmap resourceBitmap;
private Stopwatch stopwatch = new Stopwatch();
// consider making these bindable props
private float percentComplete;
private float imageSize = 40;
private float columnSpacing = 100;
private float rowSpacing = 100;
private float framesPerSecond = 60;
private float cycleTime = 1; // in seconds, for a single column
public FlyingImageBackgroundSkia()
{
this.canvasView = new SKCanvasView();
this.canvasView.PaintSurface += OnCanvasViewPaintSurface;
this.Content = this.canvasView;
string resourceID = "XamarinTestProject.Resources.Images.fireTruck.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
this.resourceBitmap = SKBitmap.Decode(stream);
}
}
~FlyingImageBackgroundSkia() => this.resourceBitmap.Dispose();
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
private static async void OnPageActivenessChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackgroundSkia)bindable;
await control.AnimationLoop();
}
private async Task AnimationLoop()
{
this.stopwatch.Start();
while (IsActive)
{
this.percentComplete = (float)(this.stopwatch.Elapsed.TotalSeconds % this.cycleTime) / this.cycleTime; // always between 0 and 1
this.canvasView.InvalidateSurface(); // trigger redraw
await Task.Delay(TimeSpan.FromSeconds(1.0 / this.framesPerSecond)); // non-blocking
}
this.stopwatch.Stop();
}
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
var xPositions = Enumerable.Range(0, info.Width + (int)this.columnSpacing).Where(x => x % (int)this.columnSpacing == 0).ToList();
xPositions.Insert(0, -(int)this.columnSpacing);
var yPositions = Enumerable.Range(0, info.Height + (int)this.rowSpacing).Where(x => x % (int)this.rowSpacing == 0).ToList();
yPositions.Insert(0, -(int)this.rowSpacing);
if (this.resourceBitmap != null)
{
foreach (var xPos in xPositions)
{
var xPosNow = xPos + (this.rowSpacing * this.percentComplete);
foreach (var yPos in yPositions)
{
canvas.DrawBitmap(
this.resourceBitmap,
new SKRect(xPosNow, yPos, xPosNow + this.imageSize, yPos + this.imageSize));
}
}
}
}
}
I'm making a WPF text-editor using TextFormatter. I need to indent the second line in each paragraph.
The indentation width in the second line should be like the width of the first word on the first line, including the white space after the first word. Something like that:
Indent of second line in Indentation Inde
second line in Indentation Indenta
of second line in Indentation of second l
ine in Indentation of second line in Inde
ntation of second line in
Second thing: The last line in the paragraph should be in the center.
how to make this happen?
Thanks in advance!!
This is far from being easy. I suggest you use WPF's Advanced Text Formatting.
There is an offical (relatively poor, but it's the only one) sample: TextFormatting.
So, I have created a small sample app with a textbox and a special custom control that renders the text from the textbox simultaneously, the way you want (well, almost, see remarks at the end).
<Window x:Class="WpfApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
Title="MainWindow" Height="550" Width="725">
<StackPanel Margin="10">
<TextBox Name="TbSource" AcceptsReturn="True" TextWrapping="Wrap" BorderThickness="1"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"></TextBox>
<Border BorderThickness="1" BorderBrush="#ABADB3" Margin="0" Padding="0">
<local:MyTextControl Margin="5" Text="{Binding ElementName=TbSource, Path=Text}" />
</Border>
</StackPanel>
</Window>
I have chosen to write a custom control, but you could also build a geometry (like in the official 'TextFormatting' sample).
[ContentProperty(nameof(Text))]
public class MyTextControl : FrameworkElement
{
// I have only declared Text as a dependency property, but fonts, etc should be here
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MyTextControl),
new FrameworkPropertyMetadata(string.Empty,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure));
private List<TextLine> _lines = new List<TextLine>();
private TextFormatter _formatter = TextFormatter.Create();
public string Text { get => ((string)GetValue(TextProperty)); set { SetValue(TextProperty, value); } }
protected override Size MeasureOverride(Size availableSize)
{
// dispose old stuff
_lines.ForEach(l => l.Dispose());
_lines.Clear();
double height = 0;
double width = 0;
var ts = new MyTextSource(Text);
var index = 0;
double maxWidth = availableSize.Width;
if (double.IsInfinity(maxWidth))
{
// it means width was not fixed by any constraint above this.
// we pick an arbitrary value, we could use visual parent, etc.
maxWidth = 100;
}
double firstWordWidth = 0; // will be computed with the 1st line
while (index < Text.Length)
{
// we indent the second line
var props = new MyTextParagraphProperties(new MyTextRunProperties(), _lines.Count == 1 ? firstWordWidth : 0);
var line = _formatter.FormatLine(ts, index, maxWidth, props, null);
if (_lines.Count == 0)
{
// get first word and whitespace real width (so we can support justification / whitespaces widening, kerning)
firstWordWidth = line.GetDistanceFromCharacterHit(new CharacterHit(ts.FirstWordAndSpaces.Length, 0));
}
index += line.Length;
_lines.Add(line);
height += line.TextHeight;
width = Math.Max(width, line.WidthIncludingTrailingWhitespace);
}
return new Size(width, height);
}
protected override void OnRender(DrawingContext dc)
{
double height = 0;
for (int i = 0; i < _lines.Count; i++)
{
if (i == _lines.Count - 1)
{
// last line centered (using pixels, not characters)
_lines[i].Draw(dc, new Point((RenderSize.Width - _lines[i].Width) / 2, height), InvertAxes.None);
}
else
{
_lines[i].Draw(dc, new Point(0, height), InvertAxes.None);
}
height += _lines[i].TextHeight;
}
}
}
// this is a simple text source, it just gives back one set of characters for the whole string
public class MyTextSource : TextSource
{
public MyTextSource(string text)
{
Text = text;
}
public string Text { get; }
public string FirstWordAndSpaces
{
get
{
if (Text == null)
return null;
int pos = Text.IndexOf(' ');
if (pos < 0)
return Text;
while (pos < Text.Length && Text[pos] == ' ')
{
pos++;
}
return Text.Substring(0, pos);
}
}
public override TextRun GetTextRun(int index)
{
if (Text == null || index >= Text.Length)
return new TextEndOfParagraph(1);
return new TextCharacters(
Text,
index,
Text.Length - index,
new MyTextRunProperties());
}
public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int indexLimit) => throw new NotImplementedException();
public override int GetTextEffectCharacterIndexFromTextSourceCharacterIndex(int index) => throw new NotImplementedException();
}
public class MyTextParagraphProperties : TextParagraphProperties
{
public MyTextParagraphProperties(TextRunProperties defaultTextRunProperties, double indent)
{
DefaultTextRunProperties = defaultTextRunProperties;
Indent = indent;
}
// TODO: some of these should be DependencyProperties on the control
public override FlowDirection FlowDirection => FlowDirection.LeftToRight;
public override TextAlignment TextAlignment => TextAlignment.Justify;
public override double LineHeight => 0;
public override bool FirstLineInParagraph => true;
public override TextRunProperties DefaultTextRunProperties { get; }
public override TextWrapping TextWrapping => TextWrapping.Wrap;
public override TextMarkerProperties TextMarkerProperties => null;
public override double Indent { get; }
}
public class MyTextRunProperties : TextRunProperties
{
// TODO: some of these should be DependencyProperties on the control
public override Typeface Typeface => new Typeface("Segoe UI");
public override double FontRenderingEmSize => 20;
public override Brush ForegroundBrush => Brushes.Black;
public override Brush BackgroundBrush => Brushes.White;
public override double FontHintingEmSize => FontRenderingEmSize;
public override TextDecorationCollection TextDecorations => new TextDecorationCollection();
public override CultureInfo CultureInfo => CultureInfo.CurrentCulture;
public override TextEffectCollection TextEffects => new TextEffectCollection();
}
This is the result:
Things I have not done:
This does not support edit, it's not a textbox. This is too much work for such a small bounty :-)
Support multiple paragraphs. I've just indented the second line in my sample. You would need to parse the text to extract "paragraphs" whatever you think that is.
DPI awareness support should be added (for .NET Framework 4.6.2 or above). This is done in the 'TextFormatting' sample, you basically need to carry the PixelsPerDip value all around.
What happens in some edge cases (only two lines, etc.)
Expose usual properties (FontFamily, etc...) on the custom control
I want to create a data-binded horizontal layout ItemsControl where for each item there would be a Button. When I add new items to the collection the ItemsControl should grow, relative to the Window it is in, until it reaches it's MaxWidth property. Then all buttons should shrink equally to fit inside MaxWidth. Something similar to the tabs of a Chrome browser.
Tabs with space:
Tabs with no empty space:
So far I've gotten to this:
<ItemsControl Name="ButtonsControl" MaxWidth="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type dataclasses:TextNote}">
<Button Content="{Binding Title}" MinWidth="80"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
When adding items the expansion of the StackPanel and Window are fine, but when MaxWidth is reached the items just start to disappear.
I don't think it is possible to produce that behaviour using any combination of the standard WPF controls, but this custom StackPanel control should do the job:
public class SqueezeStackPanel : Panel
{
private const double Tolerance = 0.001;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register
("Orientation", typeof (Orientation), typeof (SqueezeStackPanel),
new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure,
OnOrientationChanged));
private readonly Dictionary<UIElement, Size> _childToConstraint = new Dictionary<UIElement, Size>();
private bool _isMeasureDirty;
private bool _isHorizontal = true;
private List<UIElement> _orderedSequence;
private Child[] _children;
static SqueezeStackPanel()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof (SqueezeStackPanel),
new FrameworkPropertyMetadata(typeof (SqueezeStackPanel)));
}
protected override bool HasLogicalOrientation
{
get { return true; }
}
protected override Orientation LogicalOrientation
{
get { return Orientation; }
}
public Orientation Orientation
{
get { return (Orientation) GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
protected override Size ArrangeOverride(Size finalSize)
{
var size = new Size(_isHorizontal ? 0 : finalSize.Width, !_isHorizontal ? 0 : finalSize.Height);
var childrenCount = Children.Count;
var rc = new Rect();
for (var index = 0; index < childrenCount; index++)
{
var child = _orderedSequence[index];
var childVal = _children[index].Val;
if (_isHorizontal)
{
rc.Width = double.IsInfinity(childVal) ? child.DesiredSize.Width : childVal;
rc.Height = Math.Max(finalSize.Height, child.DesiredSize.Height);
size.Width += rc.Width;
size.Height = Math.Max(size.Height, rc.Height);
child.Arrange(rc);
rc.X += rc.Width;
}
else
{
rc.Width = Math.Max(finalSize.Width, child.DesiredSize.Width);
rc.Height = double.IsInfinity(childVal) ? child.DesiredSize.Height : childVal;
size.Width = Math.Max(size.Width, rc.Width);
size.Height += rc.Height;
child.Arrange(rc);
rc.Y += rc.Height;
}
}
return new Size(Math.Max(finalSize.Width, size.Width), Math.Max(finalSize.Height, size.Height));
}
protected override Size MeasureOverride(Size availableSize)
{
for (var i = 0; i < 3; i++)
{
_isMeasureDirty = false;
var childrenDesiredSize = new Size();
var childrenCount = Children.Count;
if (childrenCount == 0)
return childrenDesiredSize;
var childConstraint = GetChildrenConstraint(availableSize);
_children = new Child[childrenCount];
_orderedSequence = Children.Cast<UIElement>().ToList();
for (var index = 0; index < childrenCount; index++)
{
if (_isMeasureDirty)
break;
var child = _orderedSequence[index];
const double minLength = 0.0;
const double maxLength = double.PositiveInfinity;
MeasureChild(child, childConstraint);
if (_isHorizontal)
{
childrenDesiredSize.Width += child.DesiredSize.Width;
_children[index] = new Child(minLength, maxLength, child.DesiredSize.Width);
childrenDesiredSize.Height = Math.Max(childrenDesiredSize.Height, child.DesiredSize.Height);
}
else
{
childrenDesiredSize.Height += child.DesiredSize.Height;
_children[index] = new Child(minLength, maxLength, child.DesiredSize.Height);
childrenDesiredSize.Width = Math.Max(childrenDesiredSize.Width, child.DesiredSize.Width);
}
}
if (_isMeasureDirty)
continue;
var current = _children.Sum(s => s.Val);
var target = GetSizePart(availableSize);
var finalSize = new Size
(Math.Min(availableSize.Width, _isHorizontal ? current : childrenDesiredSize.Width),
Math.Min(availableSize.Height, _isHorizontal ? childrenDesiredSize.Height : current));
if (double.IsInfinity(target))
return finalSize;
RecalcChilds(current, target);
current = 0.0;
for (var index = 0; index < childrenCount; index++)
{
var child = _children[index];
if (IsGreater(current + child.Val, target, Tolerance) &&
IsGreater(target, current, Tolerance))
{
var rest = IsGreater(target, current, Tolerance) ? target - current : 0.0;
if (IsGreater(rest, child.Min, Tolerance))
child.Val = rest;
}
current += child.Val;
}
RemeasureChildren(finalSize);
finalSize = new Size
(Math.Min(availableSize.Width, _isHorizontal ? target : childrenDesiredSize.Width),
Math.Min(availableSize.Height, _isHorizontal ? childrenDesiredSize.Height : target));
if (_isMeasureDirty)
continue;
return finalSize;
}
return new Size();
}
public static double GetHeight(Thickness thickness)
{
return thickness.Top + thickness.Bottom;
}
public static double GetWidth(Thickness thickness)
{
return thickness.Left + thickness.Right;
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
var removedUiElement = visualRemoved as UIElement;
if (removedUiElement != null)
_childToConstraint.Remove(removedUiElement);
}
private Size GetChildrenConstraint(Size availableSize)
{
return new Size
(_isHorizontal ? double.PositiveInfinity : availableSize.Width,
!_isHorizontal ? double.PositiveInfinity : availableSize.Height);
}
private double GetSizePart(Size size)
{
return _isHorizontal ? size.Width : size.Height;
}
private static bool IsGreater(double a, double b, double tolerance)
{
return a - b > tolerance;
}
private void MeasureChild(UIElement child, Size childConstraint)
{
Size lastConstraint;
if ((child.IsMeasureValid && _childToConstraint.TryGetValue(child, out lastConstraint) &&
lastConstraint.Equals(childConstraint))) return;
child.Measure(childConstraint);
_childToConstraint[child] = childConstraint;
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (SqueezeStackPanel) d;
panel._isHorizontal = panel.Orientation == Orientation.Horizontal;
}
private void RecalcChilds(double current, double target)
{
var shouldShrink = IsGreater(current, target, Tolerance);
if (shouldShrink)
ShrinkChildren(_children, target);
}
private void RemeasureChildren(Size availableSize)
{
var childrenCount = Children.Count;
if (childrenCount == 0)
return;
var childConstraint = GetChildrenConstraint(availableSize);
for (var index = 0; index < childrenCount; index++)
{
var child = _orderedSequence[index];
if (Math.Abs(GetSizePart(child.DesiredSize) - _children[index].Val) > Tolerance)
MeasureChild(child, new Size(_isHorizontal ? _children[index].Val : childConstraint.Width,
!_isHorizontal ? _children[index].Val : childConstraint.Height));
}
}
private static void ShrinkChildren(IEnumerable<Child> children, double target)
{
var sortedChilds = children.OrderBy(v => v.Val).ToList();
var minValidTarget = sortedChilds.Sum(s => s.Min);
if (minValidTarget > target)
{
foreach (var child in sortedChilds)
child.Val = child.Min;
return;
}
do
{
var tmpTarget = target;
for (var iChild = 0; iChild < sortedChilds.Count; iChild++)
{
var child = sortedChilds[iChild];
if (child.Val*(sortedChilds.Count - iChild) >= tmpTarget)
{
var avg = tmpTarget/(sortedChilds.Count - iChild);
var success = true;
for (var jChild = iChild; jChild < sortedChilds.Count; jChild++)
{
var tChild = sortedChilds[jChild];
tChild.Val = Math.Max(tChild.Min, avg);
// Min constraint skip success expand on this iteration
if (Math.Abs(avg - tChild.Val) <= Tolerance) continue;
target -= tChild.Val;
success = false;
sortedChilds.RemoveAt(jChild);
jChild--;
}
if (success)
return;
break;
}
tmpTarget -= child.Val;
}
} while (sortedChilds.Count > 0);
}
private class Child
{
public readonly double Min;
public double Val;
public Child(double min, double max, double val)
{
Min = min;
Val = val;
Val = Math.Max(min, val);
Val = Math.Min(max, Val);
}
}
}
Try using it as your ItemsPanelTemplate:
<ItemsControl Name="ButtonsControl" MaxWidth="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:SqueezeStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type dataclasses:TextNote}">
<Button Content="{Binding Title}" MinWidth="80"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I can't be sure based on the code that you have supplied, but I think you will have better layout results by removing your MaxWidth on the ItemsControl.
You can achieve something like this using a UniformGrid with Rows="1". The problem is that you can either have it stretched or not and neither of these options will do exactly what you want:
If it's stretched, then your "tabs" will always fill the whole available width. So, if you only have 1, it will be stretched across the whole width. If you set MaxWidth for the "tab", then if you have 2 they will not be adjacent but floating each in the middle of its column.
If it's left-aligned, then it will be difficult to get any padding/margin in your control, because when it shrinks, the padding will stay, making the actual content invisible.
So basically you need a control that has a "preferred" width:
When it has more space available than this preferred width, it sets itself to the preferred width.
When it has less space, it just takes up all the space it has.
This cannot be achieved using XAML (as far as I can tell), but it's not too difficult to do in code-behind. Let's create a custom control for the "tab" (namespaces omitted):
<ContentControl x:Class="WpfApplication1.UserControl1">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Border BorderBrush="Black" BorderThickness="1" Padding="0,5">
<ContentPresenter HorizontalAlignment="Center" Content="{TemplateBinding Content}"></ContentPresenter>
</Border>
</ControlTemplate>
</ContentControl.Template>
Code behind:
public partial class UserControl1 : ContentControl
{
public double DefaultWidth
{
get { return (double)GetValue(DefaultWidthProperty); }
set { SetValue(DefaultWidthProperty, value); }
}
public static readonly DependencyProperty DefaultWidthProperty =
DependencyProperty.Register("DefaultWidth", typeof(double), typeof(UserControl1), new PropertyMetadata(200.0));
public UserControl1()
{
InitializeComponent();
}
protected override Size MeasureOverride(Size constraint)
{
Size baseSize = base.MeasureOverride(constraint);
baseSize.Width = Math.Min(DefaultWidth, constraint.Width);
return baseSize;
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
Size baseBounds = base.ArrangeOverride(arrangeBounds);
baseBounds.Width = Math.Min(DefaultWidth, arrangeBounds.Width);
return baseBounds;
}
}
Then, you can create your ItemsControl, using a UniformGrid as the container:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:UserControl1 Content="{Binding}" Margin="0,0,5,0" DefaultWidth="150"></local:UserControl1>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" HorizontalAlignment="Left"></UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Here's a screenshot of the result with 3 items and many items (don't feel like counting them :)
I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.
The problem I'm facing is, when there are a great number of items (e.g. could be 5000 to 50000) there is a lag while it is rendering the markers.
With the following code it basically renders as per the selected items index, number of items and height of the track. Obviously this is inefficient and am looking for other solutions.
This is my customized vertical scrollbar
<helpers:MarkerPositionConverter x:Key="MarkerPositionConverter"/>
<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18" />
<RowDefinition Height="0.00001*" />
<RowDefinition MaxHeight="18" />
</Grid.RowDefinitions>
<Border Grid.RowSpan="3"
CornerRadius="2"
Background="#F0F0F0" />
<RepeatButton Grid.Row="0"
Style="{StaticResource ScrollBarLineButton}"
Height="18"
Command="ScrollBar.LineUpCommand"
Content="M 0 4 L 8 4 L 4 0 Z" />
<!--START-->
<ItemsControl VerticalAlignment="Stretch" x:Name="ItemsSelected"
ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="SlateGray" Width="9" Height="4">
<Rectangle.RenderTransform>
<TranslateTransform>
<TranslateTransform.Y>
<MultiBinding Converter="{StaticResource MarkerPositionConverter}" FallbackValue="-1000">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
<Binding Path="ActualHeight" ElementName="ItemsSelected"/>
<Binding Path="Items.Count" ElementName="GenericDataGrid"/>
</MultiBinding>
</TranslateTransform.Y>
</TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas ClipToBounds="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--END-->
<Track x:Name="PART_Track" Grid.Row="1" IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}"
Command="ScrollBar.PageUpCommand" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumb}" Margin="1,0,1,0">
<Thumb.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.BorderBrush>
<Thumb.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource ControlLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.Background>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageDownCommand" />
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton Grid.Row="3" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineDownCommand" Content="M 0 0 L 4 4 L 8 0 Z" />
</Grid>
</ControlTemplate>
This is my converter that transforms the Y position and scales accordingly if the DataGrid height changes.
public class MarkerPositionConverter: IMultiValueConverter
{
//Performs the index to translate conversion
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
//calculated the transform values based on the following
object o = (object)values[0];
DataGrid dg = (DataGrid)values[1];
double itemIndex = dg.Items.IndexOf(o);
double trackHeight = (double)values[2];
int itemCount = (int)values[3];
double translateDelta = trackHeight / itemCount;
return itemIndex * translateDelta;
}
catch (Exception ex)
{
Console.WriteLine("MarkerPositionConverter error : " + ex.Message);
return false;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
[RE-EDIT] I have tried to create a separate class for a marker canvas, for use with ObservableCollection's. Note that at present, this does not work.
XAML still the same as yesterday:
<helpers:MarkerCollectionCanvas
x:Name="SearchMarkerCanvas"
Grid.Row="1"
Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
MarkerCollection="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SearchMarkers}"/>
Canvas class, ObservableCollection changed to use object instead of double, there is a console.writeline in MarkerCollectionCanvas_CollectionChanged that never gets called:
class MarkerCollectionCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCollectionCanvas), new PropertyMetadata(null));
public ObservableCollection<object> MarkerCollection
{
get { return (ObservableCollection<object>)GetValue(MarkerCollectionProperty); }
set { SetValue(MarkerCollectionProperty, value); }
}
public static readonly DependencyProperty MarkerCollectionProperty =
DependencyProperty.Register("MarkerCollection", typeof(ObservableCollection<object>), typeof(MarkerCollectionCanvas), new PropertyMetadata(null, OnCollectionChanged));
private static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCollectionCanvas canvas = d as MarkerCollectionCanvas;
if (e.NewValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged += canvas.MarkerCollectionCanvas_CollectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged -= canvas.MarkerCollectionCanvas_CollectionChanged;
}
}
void MarkerCollectionCanvas_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Console.WriteLine("InvalidateVisual");
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCollectionCanvas), new PropertyMetadata(Brushes.DarkOrange));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || MarkerCollection == null)
return;
//Get all items
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
//Get all selected items
object[] selection = new object[MarkerCollection.Count];
MarkerCollection.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
}
This is my singleton class with SearchMarkers in it:
public class MyClass : INotifyPropertyChanged
{
public static ObservableCollection<object> m_searchMarkers = new ObservableCollection<object>();
public ObservableCollection<object> SearchMarkers
{
get
{
return m_searchMarkers;
}
set
{
m_searchMarkers = value;
NotifyPropertyChanged();
}
}
private static MyClass m_Instance;
public static MyClass Instance
{
get
{
if (m_Instance == null)
{
m_Instance = new MyClass();
}
return m_Instance;
}
}
private MyClass()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And this is a textbox text changed behavior. This is where the ObservableCollection SearchMarkers gets populated.
public class FindTextChangedBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
}
protected override void OnDetaching()
{
AssociatedObject.TextChanged -= OnTextChanged;
base.OnDetaching();
}
private void OnTextChanged(object sender, TextChangedEventArgs args)
{
var textBox = (sender as TextBox);
if (textBox != null)
{
DataGrid dg = DataGridObject as DataGrid;
string searchValue = textBox.Text;
if (dg.Items.Count > 0)
{
var columnBoundProperties = new List<KeyValuePair<int, string>>();
IEnumerable<DataGridColumn> visibleColumns = dg.Columns.Where(c => c.Visibility == System.Windows.Visibility.Visible);
foreach (var col in visibleColumns)
{
if (col is DataGridTextColumn)
{
var binding = (col as DataGridBoundColumn).Binding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
else if (col is DataGridComboBoxColumn)
{
DataGridComboBoxColumn dgcbc = (DataGridComboBoxColumn)col;
var binding = dgcbc.SelectedItemBinding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
}
Type itemType = dg.Items[0].GetType();
if (columnBoundProperties.Count > 0)
{
ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
var itemsSource = dg.Items as IEnumerable;
Task.Factory.StartNew(() =>
{
ClassPropTextSearch.init(itemType, columnBoundProperties);
if (itemsSource != null)
{
foreach (object o in itemsSource)
{
if (ClassPropTextSearch.Match(o, searchValue))
{
tempItems.Add(o);
}
}
}
})
.ContinueWith(t =>
{
Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
});
}
}
}
}
public static readonly DependencyProperty DataGridObjectProperty =
DependencyProperty.RegisterAttached("DataGridObject", typeof(DataGrid), typeof(FindTextChangedBehavior), new UIPropertyMetadata(null));
public object DataGridObject
{
get { return (object)GetValue(DataGridObjectProperty); }
set { SetValue(DataGridObjectProperty, value); }
}
}
Here you go, I tried to attempt it for you.
Created a class MarkerCanvas deriving Canvas with a property to bind with the data grid
Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual
overrided the method OnRender to take control of drawing and did the necessary check and calculation
finally rendered the rectangle on the calculated coordinates using the given brush
MarkerCanvas class
class MarkerCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
// Using a DependencyProperty as the backing store for Grid. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCanvas), new PropertyMetadata(null, OnGridChanged));
private static void OnGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCanvas canvas = d as MarkerCanvas;
if (e.NewValue != null)
{
(e.NewValue as DataGrid).SelectionChanged += canvas.MarkerCanvas_SelectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as DataGrid).SelectionChanged -= canvas.MarkerCanvas_SelectionChanged;
}
}
void MarkerCanvas_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
// Using a DependencyProperty as the backing store for MarkerBrush. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCanvas), new PropertyMetadata(Brushes.SlateGray));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}
}
I have also adjusted the height of marker so it grows when there are less items, you can choose to fix it to specific value as per your needs
in XAML replace your items control with the new marker canvas with binding to the grid
<helpers:MarkerCanvas Grid.Row="1" Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
helpers: is referring to WpfAppDataGrid.Helpers where I create the class, you can choose your own namespace
also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray
rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method.
Also to skip some of the overlapping rectangles to be rendered you can change the method like this. little buggy as of now
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Performance optimization
I tried to optimize the performance using a slight different approach, specially for the select all button
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
object[] selection = new object[Grid.SelectedItems.Count];
Grid.SelectedItems.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Reflection approach
in this I have tried to get the underlying selection list and attempted to retrieve the selected indexes from the same, also added even more optimization when doing select all
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
List<int> indexes = new List<int>();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double height = Math.Max(translateDelta, 4);
int itemInOneRect = (int)Math.Floor(height / translateDelta);
itemInOneRect -= (int)(itemInOneRect * 0.2);
if (Grid.SelectedItems.Count == Grid.Items.Count)
{
for (int i = 0; i < Grid.Items.Count; i += itemInOneRect)
{
indexes.Add(i);
}
}
else
{
FieldInfo fi = Grid.GetType().GetField("_selectedItems", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
IEnumerable<object> internalSelectionList = fi.GetValue(Grid) as IEnumerable<object>;
PropertyInfo pi = null;
int lastIndex = int.MinValue;
foreach (var item in internalSelectionList)
{
if (pi == null)
{
pi = item.GetType().GetProperty("Index", BindingFlags.Instance | BindingFlags.NonPublic);
}
int newIndex = (int)pi.GetValue(item);
if (newIndex > (lastIndex + itemInOneRect))
{
indexes.Add(newIndex);
lastIndex = newIndex;
}
}
indexes.Sort();
}
double width = ActualWidth;
Brush dBrush = MarkerBrush;
foreach (int itemIndex in indexes)
{
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}
I would like to know how to disable ComboBox DropDown Button Programmatically. I had seen many similar subjects but all of these have a XAML solution.
By the way, if someone know how to disable all ComboBox control design and left visible only item template it can be helpful too.
UPDATE
its my XAML definition
<ComboBox Name="lang_ComboBox" SelectionChanged="LanguageSelection_ComboBox_SelectionChanged"/>
And there is how i use it:
String text = "dorf";
BitmapImage image = new BitmapImage(new Uri("http://img88.imageshack.us/img88/4351/butchermi4.png"));
lang_ComboBox.Width = 100;
lang_ComboBox.Height = 30;
Grid sp;
for (int i = 0; i < 5; i++)
{
ColumnDefinition gridCol1 = new ColumnDefinition();
gridCol1.Width = new GridLength(30.0);
ColumnDefinition gridCol2 = new ColumnDefinition();
gridCol2.Width = new GridLength(70.0);
sp = new Grid()
{
Width = 100,
Height = 30
};
Image im = new Image()
{
Source = image,
Width = 25,
Height = 25
};
Label la = new Label()
{
Content = text
};
sp.ColumnDefinitions.Add(gridCol1);
sp.ColumnDefinitions.Add(gridCol2);
Grid.SetColumn(im, 0);
Grid.SetColumn(la, 1);
sp.Children.Add(la);
sp.Children.Add(im);
lang_ComboBox.Items.Add(sp);
}
UPDATE 2
Hmmm I get it now, I use wrong word. It should be "Hide" control design and still can choose from a list. My bad sorry. But i know how i can solve it with Anatoliy Nokolaev's Code. To hide control design i use:
ToggleButton dropDownButton = GetFirstChildOfType<ToggleButton>(lang_ComboBox);
dropDownButton.Visibility = System.Windows.Visibility.Collapsed;
Unwanted behavior is now only that i cant show combobox dropdownmenu, but I'll invoke it programmatically by add on click event and should be good.
If there is any easiest way to do this tell me :).
To disable only the ToggleButton in ComboBox programmatically, you need to find this in the ComboBox control using VisualTreeHelper and assign a property IsEnabled to false, like this:
XAML
<Window x:Class="DisableComboBoxButton.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"
Loaded="Window_Loaded">
<StackPanel>
<ComboBox Name="comboBox"
Width="100"
Height="25"
SelectedIndex="0">
<ComboBoxItem>Test1</ComboBoxItem>
<ComboBoxItem>Test2</ComboBoxItem>
<ComboBoxItem>Test3</ComboBoxItem>
</ComboBox>
<ComboBox Name="AllComboBoxDisabled"
Width="100"
Height="25"
IsEnabled="False"
SelectedIndex="0">
<ComboBoxItem>Test1</ComboBoxItem>
<ComboBoxItem>Test2</ComboBoxItem>
<ComboBoxItem>Test3</ComboBoxItem>
</ComboBox>
</StackPanel>
</Window>
Code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ToggleButton dropDownButton = GetFirstChildOfType<ToggleButton>(comboBox);
dropDownButton.IsEnabled = false;
}
public static T GetFirstChildOfType<T>(DependencyObject dependencyObject) where T : DependencyObject
{
if (dependencyObject == null)
{
return null;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var result = (child as T) ?? GetFirstChildOfType<T>(child);
if (result != null)
{
return result;
}
}
return null;
}
}
Output
Notes
Always use GetFirstChildOfType() function only when the control will be fully loaded, otherwise it will not find it and give null. In this case, I put this code in the event Window_Loaded which says that all the controls of the Window successfully load.
Edit: another version
Not to say that this version is easier to implement, but it would be more correct and a bit easier to use.
So, we need a template for your ComboBox, because it allows access to elements that are within the control. Just like that, the ToggleButton can not be accessed from both the code and of XAML.
We create attached dependency property that will serve the current ComboBox another property, such as which will give access to our button Visibility.
Our property Visibility:
public static class ButtonExt
{
public static readonly DependencyProperty VisibilityProperty;
public static void SetVisibility(DependencyObject DepObject, Visibility value)
{
DepObject.SetValue(VisibilityProperty, value);
}
public static Visibility GetVisibility(DependencyObject DepObject)
{
return (Visibility)DepObject.GetValue(VisibilityProperty);
}
static ButtonExt()
{
PropertyMetadata VisibiltyPropertyMetadata = new PropertyMetadata(Visibility.Collapsed);
VisibilityProperty = DependencyProperty.RegisterAttached("Visibility",
typeof(Visibility),
typeof(ButtonExt),
VisibiltyPropertyMetadata);
}
}
Setter property in ComboBox template (skip version, full version see in project in App.xaml file):
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton Name="ToggleButton"
Template="{StaticResource ComboBoxToggleButton}"
IsChecked="{Binding Path=IsDropDownOpen,
Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{TemplateBinding PropertiesExtension:ButtonExt.Visibility}" // <------ Here
Grid.Column="2"
Focusable="False"
ClickMode="Press" />
Now, we are setting this property like this:
<ComboBox Name="comboBox"
Style="{StaticResource ComboBoxBaseStyle}"
PropertiesExtension:ButtonExt.Visibility="Visible"
Width="100"
Height="30"
SelectedIndex="0">
<ComboBoxItem>Test1</ComboBoxItem>
<ComboBoxItem>Test2</ComboBoxItem>
<ComboBoxItem>Test3</ComboBoxItem>
</ComboBox>
or in code-behind via Click event handlers:
private void HideButton_Click(object sender, RoutedEventArgs e)
{
ButtonExt.SetVisibility(comboBox, Visibility.Hidden);
}
private void ShowButton_Click(object sender, RoutedEventArgs e)
{
ButtonExt.SetVisibility(comboBox, Visibility.Visible);
}
Full version of example project is here.
Try with this
Dispatcher.BeginInvoke(new Action(() =>
{
ToggleButton dropDownButton = GetFirstChildOfType<ToggleButton>(cboMedicos);
if (dropDownButton != null)
{
dropDownButton.IsEnabled = false;
}
}), System.Windows.Threading.DispatcherPriority.Render);
public static T GetFirstChildOfType<T>(DependencyObject dependencyObject) where T : DependencyObject
{
if (dependencyObject == null)
{
return null;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var result = (child as T) ?? GetFirstChildOfType<T>(child);
if (result != null)
{
return result;
}
}
return null;
}