Disable ComboBox Without Changing Appearance - c#

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);
}
}
}

Related

Calendar control selected date WPF

I am trying to bind my MouseDoubleClick event to whenever the user double clicks only on a day in the calendar, which will open a new window for that day. However the latter is performed and fetches the highlighted day even if the user double clicks anywhere else in the calendar area.
I tried to do it using the style option however I am getting the same result as if I place it in the calendar definition line:
<Calendar x:Name="calendar" Grid.Column="1" HorizontalAlignment="Stretch"
Margin="10,7,0,0" VerticalAlignment="Top" IsTodayHighlighted="True"
MouseDoubleClick="event">
Same Result as
<Style TargetType="CalendarDayButton">
<EventSetter Event="MouseDoubleClick" Handler="Cdb_MouseDoubleClick"/>
</Style>
How can I differentiate between when a day is press, when the month is pressed, when nothing is pressed, instead of what is focused?
EDIT (this method is working using xaml):
<Calendar x:Name="calendar" Grid.Column="1" HorizontalAlignment="Stretch"
Margin="10,7,0,0" VerticalAlignment="Top"
IsTodayHighlighted="True" SelectionMode="SingleDate">
<Calendar.CalendarDayButtonStyle>
<Style TargetType="CalendarDayButton">
<EventSetter Event="MouseDoubleClick" Handler="CalendarDayButton_MouseDoubleClick"/>
</Style>
</Calendar.CalendarDayButtonStyle>
</Calendar>
private void CalendarDayButton_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Test");
}
For something like this, I generally look at e.OriginalSource, then walk up the visual tree to find the target parent type, in your case CalendarDayButton. The original source is generally a TextBlock or some primitive, as that is what user actually clicks on. Also, there is no need for applying a style to CalendarDayButton.
So if you put the double click event handler on your Calendar as per your first line of code, you can do it like below. There, if a visual parent of the is not found, FindParentOfType() method will return null. Then it is just a matter of testing for null. If not null, means you have the correct target.
<Calendar x:Name="calendar" Grid.Column="1" HorizontalAlignment="Stretch"
Margin="10,7,0,0" VerticalAlignment="Top" IsTodayHighlighted="True"
MouseDoubleClick="calendar_MouseDoubleClick">
private void calendar_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
DependencyObject originalSource = e.OriginalSource as DependencyObject;
CalendarDayButton day = FindParentOfType<CalendarDayButton>(originalSource);
if (day != null)
{
//open menu
}
e.Handled = true;
}
//and you will need this helper method
//generally a staple in any WPF programmer's arsenal
public static T FindParentOfType<T>(DependencyObject source) where T : DependencyObject
{
T ret = default(T);
DependencyObject parent = VisualTreeHelper.GetParent(source);
if (parent != null)
{
ret = parent as T ?? FindParentOfType<T>(parent) as T;
}
return ret;
}

How can I override DataGrid selection behavior?

I would like to modify the selection behavior of the DataGrid in the following way. Normally when you have multiple rows selected, and then you click one of the items already selected, the selection is reset to only the clicked item. I would like to change it such that if one of the multi-selected rows is clicked without any keyboard modifiers, the selection is not modified. The goal of this is to allow a multi-item drag-drop.
I noticed that when aforementioned default behavior is activated, the call stack includes:
at System.Windows.Controls.DataGrid.OnSelectionChanged(SelectionChangedEventArgs e)
at System.Windows.Controls.Primitives.Selector.SelectionChanger.End()
at System.Windows.Controls.DataGrid.MakeFullRowSelection(ItemInfo info, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)
at System.Windows.Controls.DataGrid.HandleSelectionForCellInput(DataGridCell cell, Boolean startDragging, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)
at System.Windows.Controls.DataGridCell.OnAnyMouseLeftButtonDown(MouseButtonEventArgs e)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
at System.Windows.UIElement.ReRaiseEventAs(DependencyObject sender, RoutedEventArgs args, RoutedEvent newEvent)
at System.Windows.UIElement.OnMouseDownThunk(Object sender, MouseButtonEventArgs e)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
Therefore it looks like I should be able to modify the behavior by overriding DataGridCell.OnMouseLeftButtonDown, something like this:
class MultiDragDataGridCell : DataGridCell
{
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
// This allows users to click-and-drag a multi-selection by handling the event before
// the default behavior (deselecting everything but the clicked cell) kicks in.
if (IsSelected && Keyboard.Modifiers == ModifierKeys.None)
{
e.Handled = true;
}
base.OnMouseLeftButtonDown(e);
}
}
However, I'm having trouble getting the DataGrid to create a MultiDragDataGridCell instead of a normal DataGridCell, since the class that instantiates DataGridCell is internal. Anyone know how I can achieve that, or if there's another way of achieving the behavior I want?
Other things I tried:
Styling the DataGridCell to add a handler to MouseLeftButtonDown. This doesn't work because it executes after the selection has already changed.
Styling the DataGridCell to add a handler to PreviewMouseLeftButtonDown. This works, but it prevents me from clicking any buttons, etc. inside the cell.
Note: This answer only tries to provide a solution to following issue mentioned in the question; not how to override the grid's selection behavior. I am hoping that once you have a custom DataGridCell in place, it can be a good starting point for what you are trying to do.
However, I'm having trouble getting the DataGrid to create a MultiDragDataGridCell instead of a normal DataGridCell, since the class that instantiates DataGridCell is internal. Anyone know how I can achieve that..
Solution: In order to ensure that the DataGrid uses your custom DataGridCell - you need to re-template your DataGridRow to use an extended version of DataGridCellsPresenter which in-turn will provide your custom DataGridCell.
Please refer following sample code:
Extending DataGrid controls
public class ExtendedDataGrid : DataGrid
{
protected override DependencyObject GetContainerForItemOverride()
{
//This provides the DataGrid with a customized version for DataGridRow
return new ExtendedDataGridRow();
}
}
public class ExtendedDataGridRow : DataGridRow { }
public class ExtendedDataGridCellsPresenter : System.Windows.Controls.Primitives.DataGridCellsPresenter
{
protected override DependencyObject GetContainerForItemOverride()
{
//This provides the DataGrid with a customized version for DataGridCell
return new ExtendedDataGridCell();
}
}
public class ExtendedDataGridCell : DataGridCell
{
// Your custom/overridden implementation can be added here
}
Re-template DataGridRow in XAML (a more comprehensive template can be found at this link - I am only using a watered-down version of it for sake of readability).
<Style TargetType="{x:Type local:ExtendedDataGridRow}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ExtendedDataGridRow}">
<Border x:Name="DGR_Border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<SelectiveScrollingGrid>
<SelectiveScrollingGrid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</SelectiveScrollingGrid.ColumnDefinitions>
<SelectiveScrollingGrid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</SelectiveScrollingGrid.RowDefinitions>
<!-- Make sure to register your custom DataGridCellsPresenter here as following -->
<local:ExtendedDataGridCellsPresenter Grid.Column="1"
ItemsPanel="{TemplateBinding ItemsPanel}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<DataGridDetailsPresenter Grid.Column="1"
Grid.Row="1"
Visibility="{TemplateBinding DetailsVisibility}"
SelectiveScrollingGrid.SelectiveScrollingOrientation=
"{Binding AreRowDetailsFrozen,
ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical},
Converter={x:Static DataGrid.RowDetailsScrollingConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
<DataGridRowHeader Grid.RowSpan="2"
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
Visibility="{Binding HeadersVisibility,
ConverterParameter={x:Static DataGridHeadersVisibility.Row},
Converter={x:Static DataGrid.HeadersVisibilityConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
</SelectiveScrollingGrid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And, your extended DataGrid's visual tree have the custom datagrid-cells:
Also, please note, it is not mandatory to extend DataGrid, or DataGridRow to provide a custom DataGridCell - you can achieve the same result by just extending DataGridCellsPresenter (and, updating DataGridRow's control-template to use the extended version)
Actually you had a solution: make a styling for DataGridCell and set an event handler, but I suppose there was a logical error in your event handler: you have set e.Handled to true, if DataGridCell was selected, so the inner controls could not be manipulated, because the default behavior for DataGrid is first select/unselect the row/cell(and only then manipulate the inner controls), so in case you have multiple selection the clicked upon row/cell was selected, so actually you only have needed to prevent selection of row/cell clicked upon in case of multiple selection.
I suppose this should work as you have expected:
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="PreviewMouseDown"/>
</Style>
</DataGrid.Resources>
private void PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var cell = sender as DataGridCell; if (cell == null) { return; }
DataGrid parGrid = null;
var visParent = VisualTreeHelper.GetParent(cell);
while (parGrid==null && visParent != null)
{
parGrid = visParent as DataGrid;
visParent = VisualTreeHelper.GetParent(visParent);
}
if (parGrid==null) { return; }
e.Handled = cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None && parGrid.SelectedItems.Count > 1;
}
The only thing I could come up with feels like a big hack, so better don't use it as is. But it might be a starting point for finding your own solution.
Basic ideas:
Execute some event handlers even on handled events with EventManager.RegisterClassHandler. This needs some refinement or you end up messing with all cells in the whole application
Register for cell selection restore when left mouse click on selected cell without modifiers
Consider drag & drop only after left mouse click on selected cell (otherwise the user experience becomes REALLY strange for this combination of requirements)
Restore selected cells if previously registered and cells are unselected
Remove cell selection restore registration after restoring or when mouse does other things (mouse up or mouse move)
Customized data grid code:
public class MyDataGrid : DataGrid
{
static MyDataGrid()
{
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(PreviewMouseLeftButtonDownHandler));
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(MouseLeftButtonUpHandler), true);
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.MouseMoveEvent, new MouseEventHandler(MouseMoveHandler), true);
}
private static bool restoreNextCells = false;
private static bool isSelectedCell = false;
private static void PreviewMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e)
{
var cell = sender as DataGridCell;
isSelectedCell = cell.IsSelected;
restoreNextCells = cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None;
}
private static void MouseMoveHandler(object sender, MouseEventArgs e)
{
var cell = sender as DataGridCell;
if (isSelectedCell && e.LeftButton == MouseButtonState.Pressed && cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None)
{
DragDrop.DoDragDrop(cell, new ObjectDataProvider(), DragDropEffects.All);
}
restoreNextCells = false;
isSelectedCell = false;
}
private static void MouseLeftButtonUpHandler(object sender, MouseButtonEventArgs e)
{
restoreNextCells = false;
isSelectedCell = false;
}
protected override void OnSelectedCellsChanged(SelectedCellsChangedEventArgs e)
{
if (restoreNextCells && e.RemovedCells.Count > 0)
{
foreach (DataGridCellInfo item in e.RemovedCells)
{
SelectedCells.Add(item);
}
restoreNextCells = false;
}
base.OnSelectedCellsChanged(e);
}
}
Use with multi cell selection.
<local:MyDataGrid SelectionMode="Extended" SelectionUnit="Cell">
Hope I didn't leave out any important part in my explanation... ask if anything is unclear.

Scrolling to an element of a virtualising ItemsControl

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();
}

WPF EditingCommands is not working when RichTextBox is just load/empty?

Here is a very simple code example:
<DockPanel>
<ToolBar DockPanel.Dock="Top" IsTabStop="False">
<ToggleButton MinWidth="40" Command="EditingCommands.ToggleBold" CommandTarget="{Binding ElementName=XAMLRichBox}" TextBlock.FontWeight="Bold" IsTabStop="False">B</ToggleButton>
</ToolBar>
<RichTextBox x:Name="XAMLRichBox" SpellCheck.IsEnabled="True" MinHeight="100"/>
</DockPanel>
when I run it, after typing something into the RichTextBox, I can use the ToggleButton to get the BOLD effect, and everything is fine.
But if I click ToggleButton before typing in anything into RichTextBox (no matter RichTextBox get focus or not), although ToggleButton became Checked, my RichTextBox still using the normal style (not BOLD) until I click ToggleButton again.
Is this a bug? how can I get around? Thanks!
Mainwindow.xaml
<DockPanel>
<ToolBar
DockPanel.Dock="Top"
IsTabStop="False">
<ToggleButton
x:Name="boldButton"
Command="EditingCommands.ToggleBold"
CommandTarget="{Binding ElementName=XAMLRichBox}"
TextBlock.FontWeight="Bold"
ToolTip="Bold">
B
</ToggleButton>
</ToolBar>
<RichTextBox
x:Name="XAMLRichBox"
SpellCheck.IsEnabled="True"
SelectionChanged="SynchronizeWith"
MinHeight="100" />
</DockPanel>
Mainwindow.xaml.cs
private void SynchronizeWith(object sender, RoutedEventArgs e)
{
object currentValue = XAMLRichBox.Selection.GetPropertyValue(TextElement.FontWeightProperty);
boldButton.IsChecked = (currentValue == DependencyProperty.UnsetValue) ? false : currentValue != null && currentValue.Equals(FontWeights.Bold);
}
I found a semi-solution and I thought I would share since this problem is not answered anywhere on the web and I think many people are having issues with it.
I set a Variable NewInput in the constructor. When the first input in the richTextBox will be fired, I'll just apply every formating I need to it and pass it to the control.
private bool NewInput;
private void richTxt_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (NewInput)
{
richTxt.BeginChange();
TextPointer startPosition = richTxt.Selection.Start;
Run r = new Run(e.Text, startPosition);
if (IsSelectionBold)
{
r.FontWeight = FontWeights.Bold;
}
if (IsSelectionItalic)
{
r.FontStyle = FontStyles.Italic;
}
if (IsSelectionUnderlined)
{
r.TextDecorations = TextDecorations.Underline;
}
r.FontSize = double.Parse(SelectedFontHeight);
r.FontFamily = new FontFamily(SelectedFont);
richTxt.EndChange();
NewInput = false;
e.Handled = true;
richTxt.CaretPosition = richTxt.CaretPosition.GetPositionAtOffset(1);
}
}
I then replace the carret at the right place. Like this, the formating is kept even if there is nothing in the RichTextBox.
I'm sure it'll help somebody one day.
#Sinity was close, but that solution does not work when the caret is placed within the text block, only if it is at the very end.
There are two positions at the end of a Run: Run.ContentEnd and Run.ElementEnd. It appears that the ContentEnd is "just outside" the run (so any new text entered does not take on the run's style), but ElementEnd is "just inside" the end of the run, and the typed text is added into the run.
Here is a modified solution (that applies a pending Bold style as an example) that seems to work in all cases:
private bool IsBoldStylePending { get; set; }
private void RichTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (!IsBoldStylePending)
return;
rtb.BeginChange();
Run run = new Run(e.Text, rtb.CaretPosition); // Add the next character with style
run.FontWeight = FontWeights.Bold;
rtb.EndChange();
rtb.CaretPosition = run.ElementEnd; // Keep caret just within the run
IsBoldStylePending = false;
e.Handled = true;
}

Is there a BeforeCheck for WPF checkboxes?

Some of the checkboxes on my form should not be able to be checked/unchecked by users. Is there a way for me to cancel the event before the checbox's Check event is triggered?
in winForms it was easy, just
public void cb_BeforeChecked(object sender, EventArgs e){
e.Handled = true;
}
but I cannot find anything like this in WPF...I figure you can probably do it, just need to do something fancy..
Thanks!
Why not just set IsEnabled="False"?
You can set IsHitTestVisible="False" to make it not respond to user clicks. Otherwise you can bind it to a command if viewmodel logic determines whether it is clickable.
<Grid>
<CheckBox IsHitTestVisible="False" Content="I cannot be clicked at all"/>
<CheckBox Command="{Binding DoSomethingCommand}" Content="I can be clicked if DoSomethingCanExecute returns true."/>
</Grid>
In your DataContext (Viewmodel or otherwise):
RelayCommand _DoSomethingCommand = null;
public ICommand DoSomethingCommand
{
get
{
if (_DoSomethingCommand== null)
{
_DoSomethingCommand= new RelayCommand(
param => DoSomething(),
param => DoSomethingCanExecute
);
}
return _DoSomethingCommand;
}
}
public bool DoSomethingCanExecute
{
get
{
return CheckboxShouldBeEnabled();
}
}
public void DoSomething()
{
//Checkbox has been clicked
}
This might be a bit of an overkill, but you could sub-class CheckBox and then override the OnClick() method.
Only setting IsHitTestVisible="False" just takes care of the Mouse, the users can still use the KeyBoard to tab to the CheckBox and change the value.
You should set both IsHitTestVisible="False" and Focusable="False" to disable the KeyBoard as well
You can have the check box disabled, and associate a style with the disabled check box, if the disabled look is a problem. As its already pointed in the previous posts, its good to have different looks for different states.

Categories

Resources