I have a UserControl that contains a TabControl.
<UserControl x:Class="Test.MyUC"
....
xmlns:vm="clr-namespace:Test.ViewModels"
xmlns:ikriv="clr-namespace:IKriv.Windows.Controls.Behaviors"
...
<UserControl.Resources>
<vm:MyUCVM x:Key="VM" />
</UserControl.Resources>
<UserControl.DataContext>
<StaticResourceExtension ResourceKey="VM" />
</UserControl.DataContext>
<!-- Using Ivan Krivyakov's Attached Behavior -->
<TabControl ikriv:TabContent.IsCached="True"
TabStripPlacement="Top" ItemsSource="{Binding TabList}" IsSynchronizedWithCurrentItem="True">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:MyTab1VM}">
<v:MyTab1/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MyTab2VM}">
<v:MyTab2/>
</DataTemplate>
</TabControl.Resources>
...
Of course, in MyUCVM, I have TabList. Now, up to this point, everything works fine.
The problem starts when one of the tabs (e.g. MyTab1) in the TabControl needs to continuously and recursively read data from some external source (done in the ViewModel of course), and pass that data to View (via Binding) to display. Even up to this point everything is working. However, I do not want that to run when the tab is not visible, because there is no point to do that.
To do that, MyTab1VM needs to know if the associated View (MyTab1) is the selected tab. Therefore, I wired this up:
MyTab1:
<Style TargetType="TabItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=OneWayToSource}" />
</Style>
MyTab1VM
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected",
typeof(bool),
typeof(MyTab1VM),
new PropertyMetadata(false, new PropertyChangedCallback(IsSelectedChanged))
);
public bool IsSelected
{
get
{
return (bool) GetValue(IsSelectedProperty);
}
set
{
SetValue(IsSelectedProperty, value);
}
}
public static void IsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.Property == IsSelectedProperty)
{
MyTab1VM vm = d as MyTab1VM ;
vm.SetupToGetData();
}
}
private void SetupToGetData()
{
if (this.IsSelected)
{
System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(100);
timer.Tick += timer_Tick;
timer.Start();
}
}
private void timer_Tick(object sender, EventArgs e)
{
if (this.IsSelected)
this.MyData = ExternalSource.GetData();
else
{
(sender as System.Windows.Threading.DispatcherTimer).Stop();
}
}
Unfortunately, this setup only works when I set this.IsSelected = true; manually in the MyTab1VM's constructor. Leaving that out in the constructor, the data do not get shown in the view.
I have set breakpoints and confirmed that the binding for IsSelected is running correctly. Even the timer is running, and ExternalSource.GetData() is being called. But this.MyData = ExternalSource.GetData(); is not triggering the change from the ViewModel to the View.
The most puzzling part is that the same binding is triggered if IsSelected is set to true from the constructor.
Anyone out there knows what happened here?
I managed to do some fruitful troubleshooting on my own. I made a breakpoint in SetupToGetData() and I put this.GetHashCode() in my debugging watchlist. When I manually set this.IsSelected = true in the constructor, I realized that the SetupToGetData() method is called twice, with two different hash values. Planting another breakpoint in the constructor also showed that the constructor is called when I switch to this tab.
I have decided to move this to a new question, because it looks highly possible that the problem has nothing to do with binding.
Edit
Seems like I was right that this is the root of this problem. As that question is solved, so is this as well.
Related
Download repro project
Edit
The solution working sometimes completely threw me off my initial wondering why this is working in the first place. After all, the items aren't part of the Visual tree. In the end, it makes total sense:
The buttons in that collection aren't in the visual tree and thus element bindings don't work.
Applying the templates puts them into the visual tree and binding, if applied at this time, start working.
This confirms the suspected race condition.
A colleague of mine did some extended debugging that showed the issue as well - in the cases the binding succeeded, OnApplyBinding was invoked first. So using the collection without adjusting the logical tree was simply flawed.
Thanks for the replies that put back on the right track!
Original Post
I have a view control that exposes an ObservableCollection, My view can contain arbitrary elements, e.g. buttons. Note the ElementName binding on the button:
<local:ViperView>
<local:ViperView.MenuItems>
<Button Content="{Binding ElementName=btn, Path=Content}" />
</local:ViperView.MenuItems>
<Grid>
<Button x:Name="btn" Content="HELLO WORLD" />
</Grid>
</local:ViperView>
The control's ControlTemplate just renders the content using an ItemsControl:
<ControlTemplate ...
...
<ItemsControl
x:Name="PART_NavigationMenuItemsHost"
ItemsSource="{Binding MenuItems, RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>
The view above is assigned to the ActiveView property of my main view model. The main window just displays the view via data binding.
Now the problem: The ElementName binding within that view doesn't work reliably if the view is not immediately assigned to the view model after it's creation.
ElementName bindings work like this:
MainViewModel.ActiveView = new ViperView();
ElementName bindings works sometimes using normal priority:
var view = new ViperView();
Dispatcher.BeginInvoke(() => MainViewModel.ActiveView view);
ElementName binding always fails if the view model property is set with low priority:
var view = new ViperView();
Dispatcher.BeginInvoke(DispatcherPriority.Render, () => MainViewModel.ActiveView = view);
ElementName binding sometimes works if the property is set from a worker thread (Binding engine marshalls back to the UI thread):
var view = new ViperView();
Task.Factory.StartNew(() => MainViewModel.ActiveView = view);
ElementName binding always fails if the worker thread has a delay:
var view = new ViperView();
var view = new ViperView();
Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
MainViewModel.ActiveView = view;
});
I don't have an answer to this. It appears to be related to timings. For example, if I add a short Thread.Sleep to the Task sample above, this always causes the bindings to break, while without the sleep, it only sometimes breaks.
This is quite the show stopper for me - any pointer are appreciated...
Thanks for your advice
Philipp
As far as I know, ElementName binding is not updated at any time: it'll only bind to the property once and then stop updating.
This could explain your problem here: the first binding will happen (or won't) depending on the timestamp.
There is a change you can fix it by specifying the UpdateSourceTrigger property for the binding:
<Button Content="{Binding ElementName=btn, Path=Content, UpdateSourceTrigger=PropertyChanged}" />
This will make sure your binding gets updated every time btn.Content is updated.
Hope this works =)
I can't quite explain why the first option works. However I can explain why the other ones wont work.
Okay, first of all, ElementName can only work when elements are in the same visual tree. Notice that NavigationButtonItems are seperate from the actual content of ViperView.
Thus say you do:
<Button Content="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType=WpfApplication2:ViperView}}" />
(it's part of NavigationButtonItems). Now if the NavigationButtonItems and ViperView items are not blended into one(blending to one visual tree happens in ControlTemplate), then this binding would fail, and STAY as failed.
Now say the visual tree happens to be ONE as the binding is happening, then the binding will succeed and everything is nice.
Note that blending into one visual tree happens when you render the content, eg do:
dc.ActiveScreen = viperview;
Here is a quick example to demonstrate how you can do it little better:
<Button
Background="Purple"
Width="100"
Height="20"
>
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding IsReady}" Value="True">
<Setter Property="Content" Value="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType=WpfApplication2:ViperView}}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
IsReady property should be in viewModel and it essentially tells that "YES, everything is rendered as one visual tree now, you can apply bindings."
If you do the IsReady trick, the ActualWidth will start working:
Task.Factory.StartNew(() =>
{
Thread.Sleep(10);
dc.ActiveScreen = view;
//ps you might need to force wpf finish with rendering here. like use Application.DoEvents()
dc.IsReady = true;//force bindings since everything is one now.
});
Let me know if you need clarifications.
Given that the ElementName binding in part fails before the buttons in the sample are even added to the collection of the parent view, there's not much I can do to intercept the bindings. A slightly dirty workaround would be to just refresh the bindings in those controls once the template has been applied and the visual tree established:
Fron OnApplyTemplate, invoke:
internal static class BindingUtil
{
/// <summary>
/// Recursively resets all ElementName bindings on the submitted object
/// and its children.
/// </summary>
public static void ResetElementNameBindings(this DependencyObject obj)
{
IEnumerable boundProperties = obj.GetDataBoundProperties();
foreach (DependencyProperty dp in boundProperties)
{
Binding binding = BindingOperations.GetBinding(obj, dp);
if (binding != null && !String.IsNullOrEmpty(binding.ElementName)) //binding itself should never be null, but anyway
{
//just updating source and/or target doesn’t do the trick – reset the binding
BindingOperations.ClearBinding(obj, dp);
BindingOperations.SetBinding(obj, dp, binding);
}
}
int count = VisualTreeHelper.GetChildrenCount(obj);
for (int i = 0; i < count; i++)
{
//process child items recursively
DependencyObject childObject = VisualTreeHelper.GetChild(obj, i);
ResetElementNameBindings(childObject);
}
}
public static IEnumerable GetDataBoundProperties(this DependencyObject element)
{
LocalValueEnumerator lve = element.GetLocalValueEnumerator();
while (lve.MoveNext())
{
LocalValueEntry entry = lve.Current;
if (BindingOperations.IsDataBound(element, entry.Property))
{
yield return entry.Property;
}
}
}
}
Another fix, and probably preferable, would be to change the logical tree at runtime. Adding the code below to my view solves the issue, too:
public class ViperView : ContentControl
{
private readonly ObservableCollection<object> menuItems = new ObservableCollection<object>();
public ObservableCollection<object> NavigationMenuItems
{
get { return menuItems; }
}
public ViperView()
{
NavigationMenuItems.CollectionChanged += OnMenuItemsChanged;
}
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
AddLogicalChild(newItem);
}
}
}
protected override IEnumerator LogicalChildren
{
get
{
yield return this.Content;
foreach (var mi in NavigationMenuItems)
{
yield return mi;
}
}
}
}
I have a DataGrid. One of the columns is a template with a CheckBox in it. When the Checked or Unchecked events trigger, (it happens with both) the CheckBox's DataContext is sometimes null, which causes my code to error. It seems to be null most often if the mouse is moving while you press and release the button quickly (it's intermittent).
I listened for changes to the DataContext of the CheckBox by making views:ListenCheckBox (extends CheckBox) and attaching a binding, and it's never set to null, but it is set from null to a Task at times I wouldn't expect, i.e. after the DataGrid has been totally generated and you're checking/unchecking boxes. Immediately after the [un]checked event runs with a null DataContext, I get the notification that shows the DataContext changed from null to a Task, so it appears that when I get a null DataContext, it's because it hadn't actually set the DataContext by the time it ran the Checked/Unchecked event.
Also, I added Tag="{Binding}" to the CheckBox for debugging. The Tag is not null (i.e. it has the proper object) more often than the DataContext, but still not all the time.
Here are the relevant bits of the XAML code:
<navigation:Page.Resources>
<sdk:DataGridTemplateColumn x:Key="DeleteOrPrintSelect" Header="Delete Or Print Notes Selection">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<views:ListenCheckBox IsChecked="{Binding DeleteOrPrintNotesSelection, Mode=TwoWay}" Checked="DeletePrintNotesCheckBox_Changed" Unchecked="DeletePrintNotesCheckBox_Changed" HorizontalAlignment="Center" VerticalAlignment="Center" Tag="{Binding}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</navigation:Page.Resources>
<sdk:DataGrid x:Name="dataGrid1" Grid.Column="1" Grid.Row="2" AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn">
<sdk:DataGrid.RowGroupHeaderStyles>
[removed]
</sdk:DataGrid.RowGroupHeaderStyles>
</sdk:DataGrid>
And the relevant code behind:
// Create a collection to store task data.
ObservableCollection<Task> taskList = new ObservableCollection<Task>();
[code adding Tasks to taskList removed]
PagedCollectionView panelListView = new PagedCollectionView(taskList);
this.dataGrid1.ItemsSource = panelListView;
}
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "DeleteOrPrintNotesSelection")
{
e.Column = Resources["DeleteOrPrintSelect"] as DataGridTemplateColumn;
}
else
{
e.Column.IsReadOnly = true;
}
}
private void DeletePrintNotesCheckBox_Changed(object sender, RoutedEventArgs e)
{
try
{
var cb = sender as CheckBox;
var t = cb.DataContext as Task;
t.DeleteOrPrintNotesSelection = cb.IsChecked == true;
PagedCollectionView pcv = this.dataGrid1.ItemsSource as PagedCollectionView;
ObservableCollection<Task> taskList = pcv.SourceCollection as ObservableCollection<Task>;
bool anySelected = taskList.Any(x => x.DeleteOrPrintNotesSelection);
this.btnPrint.IsEnabled = anySelected;
this.btnDelete.IsEnabled = anySelected;
}
catch (Exception ex)
{
ErrorMessageBox.Show("recheck", ex, this);
}
}
Any ideas? Thanks in advance.
I found that the problem happened when you double click on the cell and it moved it to the cell editing template. In my case, I didn't have a cell editing template defined, so it used the same cell template, but instead of not changing anything, it apparently decided to make a new check box. I set the column's IsReadOnly property to true, and it fixed it. An alternate solution:
DataContext="{Binding}" (in XAML, or the code equivalent:)
cb.SetBinding(FrameworkElement.DataContextProperty, new Binding());
I'm not sure why this one fixes it, since I thought the default DataContext is {Binding}. Perhaps it's a Silverlight bug, and it gets set in a different order if you define it explicitly instead of leaving it the default.
I'm trying to perform a drag and drop approach to creating relationships in a diagram, directly analagous to SQL Server Management Studio diagramming tools. For example, in the illustration below, the user would drag CustomerID from the User entity to the Customer entity and create a foreign key relationship between the two.
The key desired feature is that a temporary arc path would be drawn as the user performs the drag operation, following the mouse. Moving entities or relationships once created isn't the issue I'm running into.
Some reference XAML corresponding to an entity on the diagram above:
<!-- Entity diagram control -->
<Grid MinWidth="10" MinHeight="10" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*" ></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" IsHitTestVisible="False" Background="{StaticResource ControlDarkBackgroundBrush}">
<Label Grid.Row="0" Grid.Column="0" Style="{DynamicResource LabelDiagram}" Content="{Binding DiagramHeader, Mode=OneWay}" />
</Grid>
<ScrollViewer Grid.Row="1" Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="{StaticResource ControlBackgroundBrush}" >
<StackPanel VerticalAlignment="Top">
<uent:EntityDataPropertiesDiagramControl DataContext="{Binding EntityDataPropertiesFolder}" />
<uent:CollectionEntityPropertiesDiagramControl DataContext="{Binding CollectionEntityPropertiesFolder}" />
<uent:DerivedEntityDataPropertiesDiagramControl DataContext="{Binding DerivedEntityDataPropertiesFolder}" />
<uent:ReferenceEntityPropertiesDiagramControl DataContext="{Binding ReferenceEntityPropertiesFolder}" />
<uent:MethodsDiagramControl DataContext="{Binding MethodsFolder}" />
</StackPanel>
</ScrollViewer>
<Grid Grid.RowSpan="2" Margin="-10">
<lib:Connector x:Name="LeftConnector" Orientation="Left" VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed"/>
<lib:Connector x:Name="TopConnector" Orientation="Top" VerticalAlignment="Top" HorizontalAlignment="Center" Visibility="Collapsed"/>
<lib:Connector x:Name="RightConnector" Orientation="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed"/>
<lib:Connector x:Name="BottomConnector" Orientation="Bottom" VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="Collapsed"/>
</Grid>
</Grid>
My current approach to doing this is to:
1) Initiate the drag operation in a child control of the entity, such as:
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
dragStartPoint = null;
}
else if (dragStartPoint.HasValue)
{
Point? currentPosition = new Point?(e.GetPosition(this));
if (currentPosition.HasValue && (Math.Abs(currentPosition.Value.X - dragStartPoint.Value.X) > 10 || Math.Abs(currentPosition.Value.Y - dragStartPoint.Value.Y) > 10))
{
DragDrop.DoDragDrop(this, DataContext, DragDropEffects.Link);
e.Handled = true;
}
}
}
2) Create a connector adorner when the drag operation leaves the entity, such as:
protected override void OnDragLeave(DragEventArgs e)
{
base.OnDragLeave(e);
if (ParentCanvas != null)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(ParentCanvas);
if (adornerLayer != null)
{
ConnectorAdorner adorner = new ConnectorAdorner(ParentCanvas, BestConnector);
if (adorner != null)
{
adornerLayer.Add(adorner);
e.Handled = true;
}
}
}
}
3) Draw the arc path as the mouse is being moved in the connector adorner, such as:
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (!IsMouseCaptured) CaptureMouse();
HitTesting(e.GetPosition(this));
pathGeometry = GetPathGeometry(e.GetPosition(this));
InvalidateVisual();
}
else
{
if (IsMouseCaptured) ReleaseMouseCapture();
}
}
The diagram Canvas is bound to a view model, and the entities and relationships on the Canvas are in turn bound to respective view models. Some XAML relating to the overall diagram:
<ItemsControl ItemsSource="{Binding Items, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<lib:DesignerCanvas VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Width" Value="{Binding Width}"/>
<Setter Property="Canvas.Height" Value="{Binding Height}"/>
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
and DataTemplates for the entites and relationships:
<!-- diagram relationship -->
<DataTemplate DataType="{x:Type dvm:DiagramRelationshipViewModel}">
<lib:Connection />
</DataTemplate>
<!-- diagram entity -->
<DataTemplate DataType="{x:Type dvm:DiagramEntityViewModel}">
<lib:DesignerItem>
<lib:EntityDiagramControl />
</lib:DesignerItem>
</DataTemplate>
Issue: The issue is that once the drag operation begins, mouse moves are no longer tracked and the connector adorner is unable to draw the arc as it does in other contexts. If I release the mouse and click again, then the arc starts drawing, but then I've lost my source object. I'm trying to figure a way to pass the source object in conjunction with mouse movement.
Bounty: Circling back to this issue, I currently plan to not use drag and drop directly to do this. I currently plan to add a DragItem and IsDragging DependencyProperty for the diagram control, which would hold the item being dragged, and flag if a drag operation is occuring. I could then use DataTriggers to change the Cursor and Adorner visibility based on IsDragging, and could use DragItem for the drop operation.
(But, I'm looking to award a bounty on another interesting approach. Please comment if more information or code is needed to clarify this question.)
Edit: Lower priority, but I'm still on the lookout for a better solution for a drag and drop diagramming approach. Want to implement a better approach in the open source Mo+ Solution Builder.
This is a fairly involved answer. Let me know if any part of it isn't clear.
I’m currently trying to solve a similar problem. In my case, I want to bind my ListBox ItemsSource to a collection and then represent every item in that collection as either a node i.e a draggable object or a connection i.e a line between nodes that redraws itself when the nodes are dragged. I’ll show you my code and detail where I think you might need to make changes to fit your needs.
Dragging
Dragging is accomplished by setting attached properties owned by the Dragger class. In my opinion, this has an advantage over using the MoveThumb to perform dragging in that making an object draggable does not involve changing its control template. My first implementation actually used MoveThumb in control templates to achieve dragging, but I found that doing so made my application very brittle (adding new features often broke the dragging). Here's the code for the Dragger:
public static class Dragger
{
private static FrameworkElement currentlyDraggedElement;
private static FrameworkElement CurrentlyDraggedElement
{
get { return currentlyDraggedElement; }
set
{
currentlyDraggedElement = value;
if (CurrentlyDraggedElement != null)
{
CurrentlyDraggedElement.MouseMove += new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
CurrentlyDraggedElement.MouseLeftButtonUp +=new MouseButtonEventHandler(CurrentlyDraggedElement_MouseLeftButtonUp);
}
}
}
private static ItemPreviewAdorner adornerForDraggedItem;
private static ItemPreviewAdorner AdornerForDraggedItem
{
get { return adornerForDraggedItem; }
set { adornerForDraggedItem = value; }
}
#region IsDraggable
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.RegisterAttached("IsDraggable", typeof(Boolean), typeof(Dragger),
new FrameworkPropertyMetadata(IsDraggable_PropertyChanged));
public static void SetIsDraggable(DependencyObject element, Boolean value)
{
element.SetValue(IsDraggableProperty, value);
}
public static Boolean GetIsDraggable(DependencyObject element)
{
return (Boolean)element.GetValue(IsDraggableProperty);
}
#endregion
#region IsDraggingEvent
public static readonly RoutedEvent IsDraggingEvent = EventManager.RegisterRoutedEvent("IsDragging", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(Dragger));
public static event RoutedEventHandler IsDragging;
public static void AddIsDraggingHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.AddHandler(Dragger.IsDraggingEvent, handler);
}
}
public static void RemoveIsDraggingEventHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.RemoveHandler(Dragger.IsDraggingEvent, handler);
}
}
#endregion
public static void IsDraggable_PropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue == true)
{
FrameworkElement element = (FrameworkElement)obj;
element.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(itemToBeDragged_MouseLeftButtonDown);
}
}
private static void itemToBeDragged_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
var element = sender as FrameworkElement;
if (element != null)
{
CurrentlyDraggedElement = element;
}
}
private static void CurrentlyDraggedElement_MouseMove(object sender, MouseEventArgs e)
{
var element = sender as FrameworkElement;
if (element.IsEnabled == true)
{
element.CaptureMouse();
//RaiseIsDraggingEvent();
DragObject(sender, new Point(Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).X,
Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).Y));
}
}
private static void CurrentlyDraggedElement_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
element.MouseMove -= new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
element.ReleaseMouseCapture();
CurrentlyDraggedElement = null;
}
private static void DragObject(object sender, Point startingPoint)
{
FrameworkElement item = sender as FrameworkElement;
if (item != null)
{
var canvas = PavilionVisualTreeHelper.GetAncestor(item, typeof(CustomCanvas)) as CustomCanvas;
double horizontalPosition = Mouse.GetPosition(canvas).X - item.ActualWidth/2;
double verticalPosition = Mouse.GetPosition(canvas).Y - item.ActualHeight/2;
item.RenderTransform = ReturnTransFormGroup(horizontalPosition, verticalPosition);
item.RaiseEvent(new IsDraggingRoutedEventArgs(item, new Point(horizontalPosition, verticalPosition), IsDraggingEvent));
}
}
private static TransformGroup ReturnTransFormGroup(double mouseX, double mouseY)
{
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform(mouseX, mouseY));
return transformGroup;
}
}
public class IsDraggingRoutedEventArgs : RoutedEventArgs
{
public Point LocationDraggedTo { get; set;}
public FrameworkElement ElementBeingDragged { get; set; }
public IsDraggingRoutedEventArgs(DependencyObject elementBeingDragged, Point locationDraggedTo, RoutedEvent routedEvent)
: base(routedEvent)
{
this.ElementBeingDragged = elementBeingDragged as FrameworkElement;
LocationDraggedTo = locationDraggedTo;
}
}
I believe that Dragger requires that the object be on a Canvas or CustomCanvas, but there isn't any good reason, besides lazyness, for this. You could easily modify it to work for any Panel. (It’s in my backlog!).
The Dragger class is also using the PavilionVisualTreeHelper.GetAncestor() helper method, which simply climbs the Visual Tree looking for the appropriate element. The code for that is below.
/// <summary>
/// Gets ancestor of starting element
/// </summary>
/// <param name="parentType">Desired type of ancestor</param>
public static DependencyObject GetAncestor(DependencyObject startingElement, Type parentType)
{
if (startingElement == null || startingElement.GetType() == parentType)
return startingElement;
else
return GetAncestor(VisualTreeHelper.GetParent(startingElement), parentType);
}
Consuming the Dragger class is very simple. Simply set Dragger.IsDraggable = true in the appropriate control’s xaml markup. Optionally, you can register to the Dragger.IsDragging event, which bubbles up from the element being dragged, to perform any processing you might need.
Updating the Connection Position
My mechanism for informing the connection that it needs to be redrawn is a little sloppy, and definitely needs readdressing.
The Connection contains two DependencyProperties of type FrameworkElement: Start and End. In the PropertyChangedCallbacks, I try to cast them as DragAwareListBoxItems (I need to make this an interface for better reusability). If the cast is successful, I register to the DragAwareListBoxItem.ConnectionDragging event. (Bad name, not mine!). When that event fires, the connection redraws its path.
The DragAwareListBoxItem doesn’t actually know when it’s being dragged, so someone has to tell it. Because of the ListBoxItem’s position in my visual tree, it never hears the Dragger.IsDragging event. So to tell it that it’s being dragged, the ListBox listens to the event and and informs the appropriate DragAwareListBoxItem.
The was going to post the code for the Connection, the DragAwareListBoxItem, and the ListBox_IsDragging, but I think it's way too much to be readable here. You can check out the project at http://code.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavilion.NodeDesigner
or clone the respository with hg clone https://code.google.com/p/pavilion/ . It's an open source project under the MIT license, so you can adapt it as you see fit. As a warning, there is no stable release, so it can change at any time.
Connectability
As with the Connection Updating, I won't paste the code. Instead, I'll tell you which classes in the project to examine and what to look for in each class.
From a user perspective, here's how creating a connection works. The user right-clicks on a node. This brings up a context menu from which the user selects "Create New Connection". That option creates a straight line whose starting point is rooted to the selected node, and whose end point follows the mouse. If the user clicks on another node, then a connection is created between the two. If the user clicks anywhere else, no connection is created and the line disappears.
Two classes are involved in this process. The ConnectionManager (which doesn't actually manage any connections) houses Attached Properties. The consuming control sets the ConnectionManager.IsConnectable property to true and sets the ConnectionManager.MenuItemInvoker property to the menu item that should start the process. Additionally, some control in your visual tree has to listen to the ConnectionPending routed event. This is where the actual creation of the connection takes place.
When the menu item is selected, the ConnectionManager creates a LineAdorner. The ConnectionManager listens to the LineAdorner LeftClick event. When that event is fired, I perform hit-testing to find the control that was selected. I then raise the ConnectionPending event, passing into the event args the two controls I want to create the connection between. It's up to the subscriber of the event to actually do the work.
I think you'll want to look into the WPF Thumb control. It wraps up some of this functionality in a convenient package.
Here's MSDN Documentation:
http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.thumb.aspx
Here's an example:
http://denisvuyka.wordpress.com/2007/10/13/wpf-draggable-objects-and-simple-shape-connectors/
Unfortunately I don't have a lot of experience in this area, but I do think that this is what you're looking for. Good luck!
As mentioned above, my current approach is to not use drag and drop directly, but to use a combination of DependencyProperties and handling mouse events to mimic a drag and drop.
The DependencyProperties in the parent diagram control are:
public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register("IsDragging", typeof(bool), typeof(SolutionDiagramControl));
public bool IsDragging
{
get
{
return (bool)GetValue(IsDraggingProperty);
}
set
{
SetValue(IsDraggingProperty, value);
}
}
public static readonly DependencyProperty DragItemProperty = DependencyProperty.Register("DragItem", typeof(IWorkspaceViewModel), typeof(SolutionDiagramControl));
public IWorkspaceViewModel DragItem
{
get
{
return (IWorkspaceViewModel)GetValue(DragItemProperty);
}
set
{
SetValue(DragItemProperty, value);
}
}
The IsDragging DependencyProperty is used to trigger a cursor change when a drag is taking place, such as:
<Style TargetType="{x:Type lib:SolutionDiagramControl}">
<Style.Triggers>
<Trigger Property="IsDragging" Value="True">
<Setter Property="Cursor" Value="Pen" />
</Trigger>
</Style.Triggers>
</Style>
Wherever I need to perform an arc drawing form of drag and drop, instead of calling DragDrop.DoDragDrop, I set IsDragging = true and DragItem to the source item being dragged.
Within the entity control on mouse leave, the connector adorner which draws the arc during the drag is enabled, such as:
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
if (ParentSolutionDiagramControl.DragItem != null)
{
CreateConnectorAdorner();
}
}
The diagram control must handle additional mouse events during the drag, such as:
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton != MouseButtonState.Pressed)
{
IsDragging = false;
DragItem = null;
}
}
The diagram control must also handle the "drop" upon a mouse up event (and it must figure out which entity is being dropped on based on mouse position), such as:
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
if (DragItem != null)
{
Point currentPosition = MouseUtilities.GetMousePosition(this);
DiagramEntityViewModel diagramEntityView = GetMouseOverEntity(currentPosition );
if (diagramEntityView != null)
{
// Perform the drop operations
}
}
IsDragging = false;
DragItem = null;
}
I am still looking for a better solution to draw the temporary arc (following the mouse) on the diagram while a drag operation is taking place.
I have a datagrid that is multi-select enabled. I need to change the selection in the viewmodel. However, the SelectedItems property is read only and can't be directly bound to a property in the viewmodel. So how do I signal to the view that the selection has changed?
Andy is correct. DataGridRow.IsSelected is a Dependency Property that can be databound to control selection from the ViewModel. The following sample code demonstrates this:
<Window x:Class="DataGridMultiSelectSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title="Window1" Height="300" Width="300">
<StackPanel>
<tk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" EnableRowVirtualization="False">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Value" Binding="{Binding Value}" />
</tk:DataGrid.Columns>
<tk:DataGrid.RowStyle>
<Style TargetType="tk:DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</tk:DataGrid.RowStyle>
</tk:DataGrid>
<Button Content="Select Even" Click="Even_Click" />
<Button Content="Select Odd" Click="Odd_Click" />
</StackPanel>
</Window>
using System.ComponentModel;
using System.Windows;
namespace DataGridMultiSelectSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new[]
{
new MyViewModel {Value = "Able"},
new MyViewModel {Value = "Baker"},
new MyViewModel {Value = "Charlie"},
new MyViewModel {Value = "Dog"},
new MyViewModel {Value = "Fox"},
};
}
private void Even_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[]) DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i%2 == 0;
}
private void Odd_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[])DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i % 2 == 1;
}
}
public class MyViewModel : INotifyPropertyChanged
{
public string Value { get; set; }
private bool mIsSelected;
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (mIsSelected == value) return;
mIsSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Be sure to set EnableRowVirtualisation="False" on the DataGrid element, else there's a risk that the IsSelected bindings fall out of kilter.
I haven't worked with the DataGrid much, but one technique that works for the ListView is to bind to the IsSelected property of the individual ListViewItem. Just set this to true for each object in your list, and then it will get selected.
Maybe the object that represents a row in the DataGrid also has an IsSelected property, and can be used in this way as well?
Guys, thanks for the help. My problem was solved. I think the problem is pretty common for new WPF developers, so I will restate my problem and as well as the solution in more details here just in case someone else runs into the same kind of problems.
The problem: I have a multi-select enabled datagrid of audio files. The grid has multiple column headers. The user can multi-select several row. When he clicks the Play button, the audio files will be played in the order of one the columns headers (say column A). When playback starts, the multi-select is cleared and only the currently playing file is highlighted. When playback is finished for all files, the multi-selection will be re-displayed. The playback is done in the viewmodel. As you can see, there are two problems here: 1) how to select the currently playing file from the viewmodel, and 2) how to signal to the view from the viewmodel that playback is finished and re-display the multi-selection.
The solution: To solve the first problem, I created a property in the viewmodel that is bound to the view's SelectedIndex property to select the currently playing file. To solve the second problem, I created a boolean property in the view model to indicate playback is finished. In the view's code behind, I subscribed the the boolean property's PropertyChanged event. In the event handler, the view's SelectedItems property is re-created from the saved multi-selection (the contents of SelectedItems was saved into a list and SelectedItems was cleared when playback started). At first, I had trouble re-creating SelectedItems. It turned out the problem was due to the fact that re-creation was initiated through a second thread. WPF does not allow that. The solution to this is to use the Dispatcher.Invoke() to let the main thread do the work. This may be a very simple problem for experienced developers, but for newbies, it's a small challenge. Anyway, a lot of help from different people.
Just use SelectedItems on any MultiSelector derived class , and use methods Add, Remove, Clear on IList it returns .
Despite some posts on this forum and others i cannot find something that tells me how to set the focus on a TextBox.
I have a userControl with many labels and textBoxes. When the form is loaded I want the a particular textBox to have the focus.
I have set the tabIndex but that didn't seem to work.
Any suggestions?
You can use the FocusManager.FocusedElement attached property for this purpose. Here's a piece of code that set the focus to TxtB by default.
<StackPanel Orientation="Vertical" FocusManager.FocusedElement="{Binding ElementName=TxtB}">
<TextBox x:Name="TxtA" Text="A" />
<TextBox x:Name="TxtB" Text="B" />
</StackPanel>
You can also use TxtB.Focus() in your code-behind if you don't want to do this in XAML.
You can apply this property directly on the TextBox :
<TextBox Text="{Binding MyText}" FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}"/>
I am new to using WPF and reading through the above examples I had a similar experience trying set the focus to a textbox using the xaml code examples given, i.e. all the examples above didn't work.
What I found was I had to place the FocusManager.FocusElement in the page element. I assume this would probably work as well if you used a Window as the parent element. Anyway, here is the code that worked for me.
<Page x:Class="NameOfYourClass"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="Title"
Height="720"
Width="915"
Background="white"
Loaded="pgLoaded"
FocusManager.FocusedElement="{Binding ElementName=NameOfYourTextBox}">
<!-- Create child elements here. -->
</Page>
I have a TextBox inside a Grid inside a DataTemplate which I want to have keyboard focus when it becomes visible. I also found that
<DataTemplate x:Key="DistanceView" DataType="{x:Type vm:ROI}">
<Grid FocusManager.FocusedElement="{Binding ElementName=tbDistance}">
<TextBox x:Name="tbDistance" Grid.Column="1" Grid.Row="1" VerticalAlignment="Bottom"/>
</Grid>
</DataTemplate>
did not work for me.
However when I call Focus() in the parent ContentControl
private void ContentControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((sender as ContentControl).IsVisible)
{
(sender as ContentControl).Focus();
}
}
it starts to work and the caret is visible in the TextBox. I think the FocusScope has to be given focus for the FocusManager.FocusedElement property to have any effect.
Jerry
From experimenting around, the xaml solution
FocusManager.FocusedElement="{Binding ElementName=yourElement}"
seems to work best when you place it in the highest element in the window hierarchy (usually Window, or the Grid you place everything else in)
Usage:
local:FocusManager.FocusOnLoad="True"
public class FocusManager
{
public static readonly DependencyProperty FocusOnLoad = DependencyProperty.RegisterAttached(
"FocusOnLoad",
typeof(bool),
typeof(FocusManager),
new UIPropertyMetadata(false, new PropertyChangedCallback(OnValueChanged))
);
private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (!(sender is Control control))
return;
if ((bool) e.NewValue == false)
return;
control.Loaded += (s, e) => control.Focus();
}
public static bool GetFocusOnLoad(DependencyObject d) => (bool) d.GetValue(FocusOnLoad);
public static void SetFocusOnLoad(DependencyObject d, bool value) => d.SetValue(FocusOnLoad, value);
}
FocusManager was not in intellisense and this confused me a bit. I just typed the entire attribute and it worked.
FocusManager.FocusedElement="{Binding ElementName=MyTextBox}"
Microsoft Visual Studio Enterprise 2015 version 14.0.23107.0/C#/WPF
For completeness, there is also a way to handle this from code behind (e.g. in the case of controls that, for whatever reason, are created dynamically and don't exist in XAML). Attach a handler to the window's Loaded event and then use the ".Focus()" method of the control you want. Bare-bones example below.
public class MyWindow
{
private VisualCollection controls;
private TextBox textBox;
// constructor
public MyWindow()
{
controls = new VisualCollection(this);
textBox = new TextBox();
controls.Add(textBox);
Loaded += window_Loaded;
}
private void window_Loaded(object sender, RoutedEventArgs e)
{
textBox.Focus();
}
}
bind the element you want to point the focus in as
FocusManager.FocusedElement= "{Binding ElementName= Comobox1}"
in grid or groupbox etc
Further to my comment on Feb 04 '22, I solved it this way:
In the UserControl definitionin the XAML add a Loaded event handler. (pressing tab after Loaded= will automatically add an event handler to the code behind)
Then edit the event handler in the code behind:
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
expressionTextBox.Focus();
}
I'm hoping that WPF is clever enough to handle th unhooking of the evnt at some point, allowing the class to be garbage collected and not give rise to memory leaks, but I don't know. I'd be interested in any comments on that.