Scrolling to an element of a virtualising ItemsControl - c#

I have a ItemsControl which displays its items in a ScrollViewer, and does virtualisation. I am trying to scroll that ScrollViewer to an (offscreen, hence virtualised) item it contains. However, since the item is virtualised, it doesn't really exist on the screen and has no position (IIUC).
I have tried BringIntoView on the child element, but it doesn't scroll into view. I have also tried manually doing it with TransformToAncestor, TransformBounds and ScrollToVerticalOffset, but TransformToAncestor never returns (I guess also because of the virtualisation, because it has no position, but I have no proof of that) and code after it never executes.
Is it possible to scroll to an item with a virtualising ItemsControl? If so, how?

I've been looking at getting a ItemsControl with a VirtualizingStackPanel to scroll to an item for a while now, and kept finding the "use a ListBox" answer. I didn't want to, so I found a way to do it. First you need to setup a control template for your ItemsControl that has a ScrollViewer in it (which you probably already have if you're using an items control). My basic template looks like the following (contained in a handy style for the ItemsControl)
<Style x:Key="TheItemsControlStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="{TemplateBinding Control.Padding}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" SnapsToDevicePixels="True">
<ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False" HorizontalScrollBarVisibility="Auto">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
So I've basically got a border with a scroll viewer thats going to contain my content.
My ItemsControl is defined with:
<ItemsControl x:Name="myItemsControl" [..snip..] Style="{DynamicResource TheItemsControlStyle}" ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.IsVirtualizing="True">
Ok now for the fun part. I've created a extension method to attach to any ItemsControl to get it to scroll to the given item:
public static void VirtualizedScrollIntoView(this ItemsControl control, object item) {
try {
// this is basically getting a reference to the ScrollViewer defined in the ItemsControl's style (identified above).
// you *could* enumerate over the ItemsControl's children until you hit a scroll viewer, but this is quick and
// dirty!
// First 0 in the GetChild returns the Border from the ControlTemplate, and the second 0 gets the ScrollViewer from
// the Border.
ScrollViewer sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild((DependencyObject)control, 0), 0) as ScrollViewer;
// now get the index of the item your passing in
int index = control.Items.IndexOf(item);
if(index != -1) {
// since the scroll viewer is using content scrolling not pixel based scrolling we just tell it to scroll to the index of the item
// and viola! we scroll there!
sv.ScrollToVerticalOffset(index);
}
} catch(Exception ex) {
Debug.WriteLine("What the..." + ex.Message);
}
}
So with the extension method in place you would use it just like ListBox's companion method:
myItemsControl.VirtualizedScrollIntoView(someItemInTheList);
Works great!
Note that you can also call sv.ScrollToEnd() and the other usual scrolling methods to get around your items.

Poking around in the .NET source code leads me to recommend you the use of a ListBox and its ScrollIntoView method. The implementation of this method relies on a few internal methods like VirtualizingPanel.BringIndexIntoView which forces the creation of the item at that index and scrolls to it. The fact that many of those mechanism are internal means that if you try to do this on your own you're gonna have a bad time.
(To make the selection this brings with it invisible you can retemplate the ListBoxItems)

I know this is an old thread, but in case someone else (like me) comes across it, I figured it would be worth an updated answer that I just discovered.
As of .NET Framework 4.5, VirtualizingPanel has a public BringIndexIntoViewPublic method which works like a charm, including with pixel based scrolling. You'll have to either sub-class your ItemsControl, or use the VisualTreeHelper to find its child VirtualizingPanel, but either way it's now very easy to force your ItemsControl to scroll precisely to a particular item/index.

Using #AaronCook example, Created a behavior that works for my VirtualizingItemsControl. Here is the code for that:
public class ItemsControlScrollToSelectedBehavior : Behavior<ItemsControl>
{
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ItemsControlScrollToSelectedBehavior),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnSelectedItemsChanged)));
public object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsControlScrollToSelectedBehavior target = (ItemsControlScrollToSelectedBehavior)d;
object oldSelectedItems = e.OldValue;
object newSelectedItems = target.SelectedItem;
target.OnSelectedItemsChanged(oldSelectedItems, newSelectedItems);
}
protected virtual void OnSelectedItemsChanged(object oldSelectedItems, object newSelectedItems)
{
try
{
var sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(AssociatedObject, 0), 0) as ScrollViewer;
// now get the index of the item your passing in
int index = AssociatedObject.Items.IndexOf(newSelectedItems);
if (index != -1)
{
sv?.ScrollToVerticalOffset(index);
}
}
catch
{
// Ignore
}
}
}
and usage is:
<ItemsControl Style="{StaticResource VirtualizingItemsControl}"
ItemsSource="{Binding BoundItems}">
<i:Interaction.Behaviors>
<behaviors:ItemsControlScrollToSelectedBehavior SelectedItem="{Binding SelectedItem}" />
</i:Interaction.Behaviors>
</ItemsControl>
Helpful for those who like Behaviors and clean XAML, no code-behind.

I know I'm pretty late to the party but hopefully this may help someone else coming along looking for the solution...
int index = myItemsControl.Items.IndexOf(*your item*).FirstOrDefault();
int rowHeight = *height of your rows*;
myScrollView.ScrollToVerticalOffset(index*rowHeight);
//this will bring the given item to the top of the scrollViewer window
... and my XAML is setup like this...
<ScrollViewer x:Name="myScrollView">
<ItemsControl x:Name="myItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<!-- data here -->
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>

This is an old thread, but I would like to suggest one way:
/// <summary>
/// Scrolls to the desired item
/// </summary>
/// <param name="control">ItemsControl</param>
/// <param name="item">item</param>
public static void ScrollIntoView(this ItemsControl control, Object item)
{
FrameworkElement framework = control.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (framework == null) { return; }
framework.BringIntoView();
}

Related

Implement data virtualisation in a UWP ListView without duplicating items

I have a large ListView which is largely made InkCanvas objects, it turns out that ListView implements data virtualisation to "cleverly" unload and load items in the view depending on the visible items in the view. The problem with this is that many times the ListView caches items and when a new item is added it essentially copy items already added in the view. So in my case, if the user adds a stroke to an Inkcanvas and then adds a new InkCanvas to the ListView, the new canvas contains the strokes from the previous canvas. As reported here this is because of the data virtualisation. My ListView is implemented as follows:
<Grid HorizontalAlignment="Stretch">
<ListView x:Name="CanvasListView" IsTapEnabled="False"
IsItemClickEnabled="False"
ScrollViewer.ZoomMode="Enabled"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollMode="Enabled"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Visible"
HorizontalAlignment="Stretch">
<!-- Make sure that items are not clickable and centered-->
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<local:CanvasControl Margin="0 2"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
MinWidth="1000" MinHeight="100" MaxHeight="400"
Background="LightGreen"/>
<Grid HorizontalAlignment="Stretch" Background="Black" Height="2"></Grid>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<InkToolbar x:Name="inkToolbar"
VerticalAlignment="Top"
Background="LightCoral"/>
<StackPanel HorizontalAlignment="Right">
<Button x:Name="AddButton" Content="Add Page" Click="Button_Click"/>
<TextBlock x:Name="PageCountText" />
</StackPanel>
</StackPanel>
</Grid>
A full example can be found here and here is a video of the issue.
Indeed if I turn off data virtualisation (or switch to an ItemsControl) everything works brilliantly. The problem however is that with a very large list, this approach has a heavy impact on performance (with 60+ InkCanvas controls the app just crashes). So is there a way to retain data virtualisation while avoiding the duplication of items? I have tried with VirtualizationMode.Standard but items are still duplicated.
To solve this problem, we must first understand why this problem occurs.
ListView has a reuse container inside, it will not endlessly create new list items, but will recycle.
In most cases, such recycling is not a problem. But it's special for InkCanvas.
InkCanvas is a stateful control. When you draw on InkCanvas, the handwriting is retained and displayed on the UI.
If your control is a TextBlock, this problem does not occur, because we can directly bind the value to TextBlock.Text, but for the Stroke of InkCanvas, we cannot directly bind, which will cause the so-called residue.
So in order to avoid this, we need to clear the state, that is, every time the InkCanvas is created or reloaded, the strokes in the InkCanvas are re-rendered.
1. Create a list for saving stroke information in ViewModel
public class ViewModel : INotifyPropertyChanged
{
// ... other code
public List<InkStroke> Strokes { get; set; }
public ViewModel()
{
Strokes = new List<InkStroke>();
}
}
2. Change the internal structure of CanvasControl
xaml
<Grid>
<InkCanvas x:Name="inkCanvas"
Margin="0 2"
MinWidth="1000"
MinHeight="300"
HorizontalAlignment="Stretch" >
</InkCanvas>
</Grid>
xaml.cs
public sealed partial class CanvasControl : UserControl
{
public CanvasControl()
{
this.InitializeComponent();
// Set supported inking device types.
inkCanvas.InkPresenter.InputDeviceTypes =
Windows.UI.Core.CoreInputDeviceTypes.Mouse |
Windows.UI.Core.CoreInputDeviceTypes.Pen;
}
private void StrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args)
{
if (Data != null)
{
var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().ToList();
Data.Strokes = strokes.Select(p => p.Clone()).ToList();
}
}
public ViewModel Data
{
get { return (ViewModel)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(ViewModel), typeof(CanvasControl), new PropertyMetadata(null,new PropertyChangedCallback(Data_Changed)));
private static void Data_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(e.NewValue!=null && e.NewValue is ViewModel vm)
{
var strokes = vm.Strokes.Select(p=>p.Clone());
var instance = d as CanvasControl;
instance.inkCanvas.InkPresenter.StrokesCollected -= instance.StrokesCollected;
instance.inkCanvas.InkPresenter.StrokeContainer.Clear();
try
{
instance.inkCanvas.InkPresenter.StrokeContainer.AddStrokes(strokes);
}
catch (Exception)
{
}
instance.inkCanvas.InkPresenter.StrokesCollected += instance.StrokesCollected;
}
}
}
In this way, we can keep our entries stable.

Setting ListView ItemsPanel Duplicates Items

I have a custom control in my application. One of the dependency properties is an ObservableCollection<ToggleButton>:
public ObservableCollection<ToggleButton> HeaderButtons
{
get { return (ObservableCollection<ToggleButton>)GetValue(HeaderButtonsProperty); }
set { SetValue(HeaderButtonsProperty, value); }
}
public static readonly DependencyProperty HeaderButtonsProperty = DependencyProperty.Register("HeaderButtons", typeof(ObservableCollection<ToggleButton>), typeof(Expandable), new PropertyMetadata(new ObservableCollection<ToggleButton>()));
I'm then putting them in a ListView in Generic.xaml:
<ListView ItemsSource="{TemplateBinding HeaderButtons}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
...and using it like this:
<controls:MyControl.HeaderButtons>
<ToggleButton x:Name="FilterButton">
<Image Source="/Assets/Icons/Empty Filter-512.png" Height="15" Width="15"/>
</ToggleButton>
</controls:MyControl.HeaderButtons>
However, I keep ending up with a duplicate item:
I can't figure out how that item is getting there. I can fix it by removing my custom ListView.ItemsPanel, but of course that makes my items flow vertically, defeating the entire purpose. Can anyone else see why this would be duplicating the item?
EDIT: For further interest, if I go into the Live Visual Tree I can see that both buttons have the name "FilterButton". Which should, of course, not be possible.
EDIT: Here's the ContentPresenter from the MainWindow:
<ContentPresenter Content="{Binding CurrentControl, Mode=OneWay}" Grid.Row="1" Grid.Column="1"/>
And CurrentControl is set to an instance of my UserControl:
private UserControl currentControl;
public UserControl CurrentControl
{
get { return currentControl; }
set
{
if (currentControl != value)
{
currentControl = value;
OnPropertyChanged("CurrentControl");
}
}
}
The source of the problem is the default value of your HeaderButtonsProperty - you set one using new PropertyMetadata(new ObservableCollection<ToggleButton>()). Contrary to what you expect it does not create one instance of the collection for each instance of your control, but a single instance shared across all of your controls.
Then you use this XAML syntax:
<controls:MyControl.HeaderButtons>
<ToggleButton (...) />
</controls:MyControl.HeaderButtons>
which does not assign a new collection to your HeadersButton property, but rather adds the specified item to the existing one. So each time this part of code is "executed", it adds a new copy of the ToggleButton to the single collection shared by all your controls.
To resolve the problem you should remove the default value from your HeaderButtonsProperty's metadata and assign a new collection instance in your control's constructor - that way each control instance will have it's own independent collection.

How can I make a multi-colored segmented progress bar in wpf?

I'm trying to create a UserControl that acts as a sort of segmented progress bar. Input would be a collection of objects, each object would have a category, a duration property, and status property. The UserControl should stretch the width and height of the parent control. Each item in the collection should represent a segment of the progress bar; color of the segment is related to the status, the width of the segment is related to the duration, and the text overlaid on the segment would be related to the category or something.
Example custom progress bar:
The text might be the collection item's ID, the top segment color would be related to status, the bottom color would be related to the category, and the width related to the duration.
Some of the options I've considered:
Make a stackpanel and somehow define each items width and wrap the whole thing in a viewbox to make it stretch the height and width. How could I control the text size, how do I make the content fit the height, how do I bind a stackpanel to a collection?
Make an attached property for a grid control that would dynamically create columns and map the collection items to the grids. Seems like a lot of work and I'm hoping theres a simpler solution since my requirements are pretty specific.
Maybe theres a way to override a uniform grid to make it non-uniform?
Maybe I should just go all code-behind and draw rectangles by iterating through my collection?
Either way, I am crossing my fingers that somebody might know a simple solution to my problem.
Here is a full working proposition of solution to the custom progress bar.
Code is here : http://1drv.ms/1QmAVuZ
1 . If all the steps are not the same width, I prefer to use Grid with columns and different widths
The columns are built dynamically based upon following class :
public class StepItem
{
public int Length { get; set; }
public int Index { get; set; }
public String Label { get; set; }
public Brush Brush { get; set; }
}
2. I chose to implement a CustomControl and inherit of ItemsControl
CustomControl because I don't want to take care of implementing of the parts of the template of the Progressbar.
ItemsControl because :
-I want to provide to ItemsSource property a collection of StepItems
-ItemsControl can have some DataTemplate as template for each item
-ItemsControl can have any Panel like Grid as template presenting the collection of items
3. The component has template in Generic.xaml
-layoutGrid wil have the "continuous rainbow"
-overlayGrid will be displayed partially over the steps depending on progression or totally over (if no progress)
-ItemsPresenter will present the collection of DataTemplates corresponding to each StepItem
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ProgressItemsControl}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="layoutGrid">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<Grid x:Name="overlayGrid" Width="100" HorizontalAlignment="Right" Background="White"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
4. Customisation of the ItemsPanel to use a Grid (instead of vertical layout)
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate >
<Grid x:Name="stepsGrid" IsItemsHost="True" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
5. In code behind of components, setting of the column width
int i = 0;
foreach (StepItem stepItem in ItemsSource)
{
total += stepItem.Length;
var columnDefinition = new ColumnDefinition() { Width = new GridLength(stepItem.Length, GridUnitType.Star) };
stepsGrid.ColumnDefinitions.Add(columnDefinition);
Grid.SetColumn(stepsGrid.Children[i], stepItem.Index);
i++;
}
6. Code behind for declaring Dependency properties that can be monitored
(excerpt)
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(ProgressItemsControl), new PropertyMetadata(0));
7. Usage of the component
<local:CustomProgressBar
x:Name="customProgressBar1"
HorizontalAlignment="Left" Height="50" Margin="32,49,0,0"
VerticalAlignment="Top" Width="379"/>
8. Feeding the component with data
private List<StepItem> stepItems = new List<StepItem>{
new StepItem{
Index=0,
Label="Step1",
Length=20,
Brush = new SolidColorBrush(Color.FromArgb(255,255,0,0)),
new StepItem{
Index=4,
Label="Step5",
Length=25,
Brush = new SolidColorBrush(Color.FromArgb(255,0,128,0)),
},
};
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
progressItemsControl1.ItemsSource = stepItems;
}
Regards

Windows Phone - How to draw shapes in dynamically created canvas - XAML vs code behind

I have a longListSelector that create several canvas dynamically and I want to draw in each canvas by using data from my ObservableCollection Games.
Here is my base code of the main page:
<Grid x:Name="ContentPanel">
<phone:LongListSelector Name="myLLS" ItemSource="{Binding GamesVM}">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel>
<Canvas /> <!-- Here I want to draw -->
<TextBlock Text="{Binding Title}"/>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
</Grid>
public class GameVM : INotifyPropertyChanged {
private string _title;
public string Title {
get { return this._title; }
set {
if (this._title!= value) {
this._title= value;
this.OnPropertyChanged("Title");
}
}
}
public void Draw() {
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
myCanvas.Children.Add(stone);
}
}
I would like to execute my Draw method when my GamesVM collection is generated but I haven't access to the corresponding canvas at this time. Putting my Draw method in code behind doesn't help because I have no event to handle where I could get both data binding object and the canvas newly generated (except if I miss something...). So I have no "myCanvas" instance in my Draw method.
I have some ideas to do that but nothing work well.
Option 1
I can put my UIElement (Ellipse, Line, etc) in an ObservableCollection which is binded in an ItemsControl like this :
<ItemsControl ItemsSource="{Binding myUIElements}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
public void Draw() {
myUIElements = new ObservableCollection<UIElement>();
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
myUIElements.Add(stone);
}
It works but when I leave the page and come back, I get an Element is already the child of another element exception.
If I use VisualTreeHelper to find my ItemsControl and call Items.Clear() on it, I get an exception too beacuse Items is read-only.
Option 2
I can use a ContentControl instead of ItemsControl and create the canvas in my Draw method:
<ContentControl Content="{Binding myUICanvas"/>
public void Draw() {
myUICanvas = new Canvas();
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
myUICanvas.Children.Add(stone);
}
It works too but when I leave the page and come back, I get a Value does not fall within the expected range exception.
I understand that I can't bind UIElement because I can't clear them when the Framework try to set them again. What is the trick to say "Please, do not add the same element twice" ?
Option 3
I can try to draw directly in XAML and bind a ViewModel object instead of UIElement object.
<ItemsControl ItemsSource="{Binding myDatas}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="{Binding Diameter}" Fill="Black" ...>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
It could work in WPF but in my Windows Phone 8 app, I have no ItemContainerStyle property to set Canvas.Left and Canvas.Right. Beside I would have to use a CompositeCollection to deal with several kind of shapes but DataType is not recognized by Visual Studio.
Moreover, even if it works with Line UIElements, the render is slower than c# approach.
So, what is the best option and how to deal with my exceptions ?
For information, I give you which one I choose.
I take option 2 and avoid the come back error by redrawing a new Canvas each time. I change my Draw definition so it return me the new Canvas.
public class GameVM : INotifyPropertyChanged {
// Title and other properties
private Canvas _myUICanvas;
public Canvas myUICanvas
{
get {
_myUICanvas = Draw();
return _myUICanvas;
}
set {
// this is never called
_myUICanvas = value;
}
}
public Canvas Draw() {
Canvas newCanvas = new Canvas();
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
newCanvas.Children.Add(stone);
return newCanvas;
}
}
Like this, I can run my program without error and without reloading/recreating all the GameVM instances.

Disable ComboBox Without Changing Appearance

Is there a way to disable changing the value of a ComboBox in WPF without giving it the visual properties of a disabled ComboBox? For example, I know that for a text field you can set the IsReadOnly property to true. Doing this for a ComboBox however, does not prevent the user from selecting a different value.
Mr. Benages, I think setting IsHitTestVisible and Focusable to false on the ComboBox might do the trick. Hope this helps.
While I agree that a disabled control should look disabled you could just set the ComboBox ControlTemplate to the standard one (or one your using) removing any of the standard functionality
eg This will give you a decent looking readonly combobox
<ComboBox>
<ComboBox.Template>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<Microsoft_Windows_Themes:ListBoxChrome x:Name="Border" Height="23" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" RenderFocused="{TemplateBinding IsKeyboardFocusWithin}" RenderMouseOver="{TemplateBinding IsMouseOver}"/>
<TextBlock FontSize="{TemplateBinding FontSize}" VerticalAlignment="Center" Text="Selected Item" Margin="5,0,0,0"></TextBlock>
</Grid>
</ControlTemplate>
</ComboBox.Template>
</ComboBox>
you'll need to include the following namespace
xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
Are you sure this is a good idea from a usability/conventions standpoint? If your goal is readability, perhaps you can change the disabled color to bump up the contrast a bit.
http://social.msdn.microsoft.com/forums/en-US/wpf/thread/45dd7614-326b-4a51-b809-d25a3ff1ade8/
Anyway, I suspect you could write an onChange event handler to reset the value to the previous entry.
Im not sure if its the same in .net however back in the VB6 days i use to get a picture box, frame or other container (sorry off the top of my head i can't remember which). I would put the combobox within that. To the users it looks the same. When you disable the container this would lock out the combo box as well, leaving it looking normal.
You can set the Foreground and Background colors and that seems to override the disabled colors. The drop down button shows as disabled which is good.
EDIT My code I tested with in IE 6/Kaxaml.
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<ComboBox Foreground="black" Background="white" IsEditable="True" Text="Hello" IsEnabled="false">
<ComboBoxItem>Test1</ComboBoxItem>
<ComboBoxItem>Test2</ComboBoxItem>
<ComboBoxItem>Test3</ComboBoxItem>
</ComboBox>
</StackPanel>
</Page>
Why then, use a comboBox.
My choice would be a label within a border that takes the place of the control, indicating that this is a display-only screen.
If it must look like a combobox, it would be better to use an object themed like a button but not clickable. You could even draw a gray dropdown arrow so it better looks like a comboBox.
It just seems overkill to actually have a combobox on the screen that people can't interact with, when a label would do fine.
...prevent the user from selecting a different value.
On top of styling you can disable keyboard input by overriding some ComboBox methods:
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
public class LockableComboBox : ComboBox
{
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (this.IsReadOnly)
{
e.Handled = true;
}
else
{
base.OnSelectionChanged(e);
}
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
if (this.IsReadOnly)
{
if ((e.Key == Key.C || e.Key == Key.Insert)
&& (Keyboard.Modifiers & ModifierKeys.Control)
== ModifierKeys.Control)
{
// Allow copy
Clipboard.SetDataObject(SelectedValue, true);
}
e.Handled = true;
}
else
{
base.OnPreviewKeyDown(e);
}
}
protected override void OnPreviewTextInput(TextCompositionEventArgs e)
{
if (this.IsReadOnly)
{
e.Handled = true;
}
else
{
base.OnPreviewTextInput(e);
}
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (this.IsReadOnly)
{
e.Handled = true;
}
else
{
base.OnKeyUp(e);
}
}
}

Categories

Resources