Jump to navigation inside a UserControl - wpf

I am currently trying to make a "jump to" menu in my app. The way I would like it to look and work is such as one at Google design book here at the top, where you have the subtitles of every part of that page in a menu (to be precise, that navigation part with such links as Contents, Baseline Grids, Keylines and Spacing etc.). I'd like it to work without splitting it into new MVVM Views if possible, all inside one UserControl. Is it possible to create some keywords and just make a menu with "goto" links like in HTML?
Baseline Grids
Maybe a custom ScrollViewer? But how would one know the height of every element in the UI?

Every framework element has a BringIntoView method. You can define a hyperlink, or button, menu, whatever that is able to invoke a command. The command would take an element as a parameter, and the command execution would call BringIntoView();
A simple example:
public class JumpToElementCommand : ICommand
{
public void Execute(object parameter)
{
FrameworkElement frameworkElement = parameter as FrameworkElement;
if (frameworkElement != null)
{
frameworkElement.BringIntoView();
}
}
public bool CanExecute(object parameter)
{
return parameter is FrameworkElement;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
Xaml Usage:
<Window.Resources>
<commands:JumpToElementCommand x:Key="JumpToElementCommand" />
</Window.Resources>
<Button Command="{StaticResource JumpToElementCommand}" CommandParameter="{Binding ElementName=FirstJump}" />
<Grid x:Name="FirstJump">
</Grid>
Edit: Added CommandManager.RequerySuggested so that the command parameter is reevaluated after loading.

Now (with great help of Michael G) I have the answer.
First of all make an ihnerited panel for the menu (for me it was parent ItemsControl with StackPanel host). Than subscribe to all Click events of the Button Items inside ItemsControl. As a CommandParameter bind to an element you want to jump to:
<Button CommandParameter="{Binding ElementName=jumpTarget}"/>
And in the Click event handler you use a solution presented here where istead of
Control_GotFocus(object sender, RoutedEventArgs e)
you pass the CommandParameter like here:
Control_GotFocus(FrameworkElement targetControl)
You also have to get the parent ScrollViewer and put it as AssociatedObject in the example above (here is explained how to get parent of selected type). Of course you can avoid an event subsciption by simply using a command with parameter added to every Button from the ItemsControl.

Related

How to implement object selection for an itemscontrol through a click action

I am in desperate need of assistance.
My app has been created in WPF and on the screen are two itemscontrols that use the same observable collection as the itemssource. One has the elements laid out in a grid and the other has the elements laid out on a ellipse based on x-y variables in the list.
However, I am stuck when trying to implement the following:
I want to click on one of the datatemplates generated on either the ellipse or the grid to select it and have BOTH corresponding elements glow or do something else to indicate a selection. (i.e. If I click on the template in the grid, both the grid and the ellipse will do something to indicate that template has been selected.)
Right now, I have been able to save the item clicked on using this type of binding in the datatemplate. The itemsource in question is contained inside ItemsSourceViewModel.
<StackPanel.InputBindings>
<MouseBinding Command="{Binding SelectImage}" CommandParameter="{Binding Path=.}" MouseAction="LeftClick"></MouseBinding>
</StackPanel.InputBindings>
Then the SelectImage Icommand that is bound to the mouseclick is implemented as such:
private ItemsSourceViewModel foo;
public SelectImage(ItemsSourceViewModel incoming)
{
this.foo = incoming;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
var obj = parameter as ImageTemplate;
foo.SelectedImage = obj;
}
So now my selected object is saved inside my instance of ItemsSourceViewModel, but how do I go about determining which template in both itemscontrols have been selected and how do I apply the triggers so they show that they have been selected?
Please help! :'(
How About defining the input binding in your dataTemplate ItemTemplate. and maybe sending the name of your template as a parameter, then you could switch on the name of your template to do different handling?

Execute custom code after command was executed

I have a TreeView control, and after a new node is added, I need to execute some custom code (ex expand the node, make it visible, and fire begind edit method). Since I want this to be available in every instance of TreeView control, I thought of subclassing the TreeView control.
Now, in order for treeview to know when the new node is added, it would either have an event that is fired when item is added (which it doesn't), or to have a reference to command that was executed to add a new item.
So two questions:
1) Is there a way to add an event in TreeView that would be fired whenever a treenode is added (I am always adding nodes through source collection from ViewModel) - I could not find any way to do this
2) I could add an AddCommand property to TreeList, that would be bound to ViewModel's AddCommand, and then have some button, or ContextMenu item that would bind to TreeList.AddCommand, instead of view model. This way TreeView would hold reference to AddCommand, but the drawback would be that actual usage would be kind of wierd. Question: How can I know when an TreeView's AddCommand (or any command, to that matter) is executed, so I can fire some custom code after it? It seems that CommandManager.AddExecutedHandler is a solution, but I am unable to execute it.
Is this any help?
public class CustomTreeControl : TreeView
{
...
// WPF only
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
Debug.WriteLine("OnItemsSourceChanged");
}
// WPF + Silverlight
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
Debug.WriteLine("OnItemsChanged: {0}", e.Action);
}
}

SL4: need to register for a move (or redraw) event on an Item in an ItemsControl

Not finding a move event or redraw event in the FrameworkElement class. And Google not helping either. So...
I have a custom ItemsControl populated by an observable collection in the VM. The ItemsControl itself leverages the
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True"/>
</i:Interaction.Behaviors>
behavior so the user can drag around the whole assembly.
When the user moves the assembly, I want to be notified by each item as the item is repositioned as a result of the assembly moving. So far I have tried registering for
this.myItem.LayoutUpdated += this.OnSomethingNeedsToUpdate;
but it doesn't seem to fire as I drag the assembly around.
Also
this.myItem.MouseMove += this.OnSomethingNeedsToUpdate;
only works if I mouse into the item which is not good enough. Because I am moving the ItemsControl and then have to go mouse into the item to get the event to fire.
Any ideas? Can I look to some ancestor in the visual tree for help in the form of a OneOfMyDecendantsWasRedrawn event or similar? Again I am trying to be notified when an item moves not be notified when the assembly moves.
I would say your best bet would be to add the MouseDragElementBehavior to your custom ItemsControl in code instead of in the Xaml. Here is how this might look (using a Grid since that is easier to demo):
public class DraggableGrid : Grid
{
public DraggableGrid()
{
Loaded += new RoutedEventHandler(DraggableGrid_Loaded);
}
void DraggableGrid_Loaded(object sender, RoutedEventArgs e)
{
MouseDragElementBehavior dragable = new MouseDragElementBehavior();
Interaction.GetBehaviors(this).Add(dragable);
dragable.Dragging += new MouseEventHandler(dragable_Dragging);
}
void dragable_Dragging(object sender, MouseEventArgs e)
{
// Custom Code Here
}
}
In the section that says Custom Code Here you would loop through you Items and notify them that they are being dragged.
I ended up writting another behavior for the individual items I care about and then wrote a LINQ query to search up the visual tree looking for ancestors with the MouseDragElementBehavior attached to them. That query found the ItemsControl since it was an eventual parent of the Item. I was then able to register for the Dragging event as desried.
Thanks again to Bryant for providing the solution over here.

WPF expand TreeView on single mouse click

I have a WPF TreeView with a HierarchicalDataTemplate.
Currently I have to double click an item to expand/collapse it.
I would like to change this behaviour to a single click, without loosing other functionality. So it should expand and collapse on click.
What is the recommended way to do this?
Thanks!
You could use a re-templated checkbox as your node (containing whatever template you are currently using) with its IsChecked property bound to the IsExpanded property of the TreeViewItem.
Here is a template I've just test that seems to do the job:
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=IsExpanded}">
<CheckBox.Template>
<ControlTemplate>
<TextBlock Text="{Binding Header}"></TextBlock>
</ControlTemplate>
</CheckBox.Template>
</CheckBox>
</HierarchicalDataTemplate>
Just replace the ControlTemplate contents with whatever you need.
If you are using a standard TreeViewItem, then you can capture the click event:
private void OnTreeViewMouseUp( object sender, MouseButtonEventArgs e )
{
var tv = sender as TreeView;
var item = tv.SelectedItem as TreeViewItem;
if( item != null )
item.IsExpanded = !item.IsExpanded;
e.Handled = true;
}
private void OnTreeViewPreviewMouseDoubleClick( object sender, MouseButtonEventArgs e )
{
e.Handled = true;
}
Most likely in your case, you'll need to do something with your binding and ViewModel. Here's a good article from CodePlex: Simplifying the WPF TreeView by Using the ViewModel Pattern.
Just use selected item changed event and use the following,
private void treeview_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
TreeViewItem item = (TreeViewItem)treeview.SelectedItem;
item.IsExpanded = true;
}
where treeview is the name of your TreeView, you could include an if to close/open based on its current state.
I have very little experience working with WPF to this point, so I am not 100% certain here. However, you might check out the .HitTest method of both the Treeview and TreeView Item (the WPF Treeview is essentially the Windows.Controls.Treeview, yes? Or a derivation thereof?).
THe HIt Test method does not always automatically appear in the Intellisense menu for a standard Windows.Forms.Treeview (I am using VS 2008) until you type most of the method name. But it should be there. You may have to experimnt.
You can use the .HitTest Method to handle the MouseDown event and return a reference to the selected treeview item. You must test for a null return, however, in case the use clicks in an area of the control which contains no Tree Items. Once you have a reference to a specific item, you should be able to set its .expanded property to the inverse of whatever it is currently. again, some experimentation may be necessary here.
As I said, I have not actually used WPF yet, so I could have this Wrong . . .
The answer of Metro Smurf (thanks to which I got where I wanted to be) suggests the right approach . You could simply hook up to the SelectedItemChanged event of the Treeview. Then cast the e.NewValue passed in the eventhandler as TreeViewItem, and access its IsExpanded property to set it to true.
void MyFavoritesTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
((TreeViewItem)e.NewValue).IsExpanded = true;
}
Then for the final touch, you can also hook up the items in your Treeview by casting them as TreeViewItem as suggested, and then you can hook up to the various manipulation events, like:
var item = tv.SelectedItem as TreeViewItem;
item.Expanded += item_Expanded;
And then do whatever you need to do in the eventhandler
void item_Expanded(object sender, RoutedEventArgs e)
{
// handle your stuff
}

Synchronizing scroll positions for 2 WPF DataGrids

I am trying to synchronize the horizontal scroll position of 2 WPF DataGrid controls.
I am subscribing to the ScrollChanged event of the first DataGrid:
<toolkit:DataGrid x:Name="SourceGrid" ScrollViewer.ScrollChanged="SourceGrid_ScrollChanged">
I have a second DataGrid:
<toolkit:DataGrid x:Name="TargetGrid">
In the event handler I was attempting to use the IScrollInfo.SetHorizontalOffset, but alas, DataGrid doesn't expose IScrollInfo:
private void SourceGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
((IScrollInfo)TargetGrid).SetHorizontalOffset(e.HorizontalOffset);
// cast to IScrollInfo fails
}
Is there another way to accomplish this? Or is there another element on TargetGrid that exposes the necessary IScrollInfo to achieve the synchronization of the scroll positions?
BTW, I am using frozen columns, so I cannot wrap both DataGrid controls with ScrollViewers.
There is great piece of code to do this:
http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx
According to the Microsoft product group, traversing the visual tree to find the ScrollViewer is the recommended method, as explained in their answer on Codeplex.
We had this same problem when using the Infragistics grid because it didn't (still doesn't) support frozen columns. So we had two grids side-by-side that were made to look as one. The grid on the left didn't scroll horizontally but the grid on the right did. Poor man's frozen columns.
Anyway, we ended up just reaching into the visual tree and pulling out the ScrollViewer ourselves. Afterall, we knew it was there - it just wasn't exposed by the object model. You could use a similar approach if the WPF grid does not expose the ScrollViewer. Or you could subclass the grid and add the functionality you require to make this work.
Interested in hearing why you need to do this.
This is a great solution. Worked fine for me in WPF.
http://www.codeproject.com/Articles/39244/Scroll-Synchronization
I just made a reference to ScrollSynchronizer dll, added a xml import:
xmlns:scroll="clr-namespace:ScrollSynchronizer"
then just added this to both my datagrids and bobs your uncle:
<DataGrid.Resources>
<Style TargetType="ScrollViewer">
<Setter Property="scroll:ScrollSynchronizer.ScrollGroup" Value="Group1" />
</Style>
</DataGrid.Resources>
You can trick the datagrid to expose its ScrollViewer as public property for each grid, when for example innerGridControl_ScrollChanged() handler called during initialisation of the usercontrol.
To expose it you can make your grid in an xaml View file, and then compose two of them in another xaml View.
Below code is on the innerGrid.xaml.cs for example:
public ScrollViewer Scroller { get; set; } // exposed ScrollViewer from the grid
private bool _isFirstTimeLoaded = true;
private void innerGridControl_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isFirstTimeLoaded) // just to save the code from casting and assignment after 1st time loaded
{
var scroller = (e.OriginalSource) as ScrollViewer;
Scroller = scroller;
_isFirstTimeLoaded = false;
}
}
on OuterGridView.xaml put an attached event handler definition:
<Views:innerGridView Grid.Row="1" Margin="2,0,2,2" DataContext="{Binding someCollection}"
x:Name="grid1Control"
ScrollViewer.ScrollChanged="Grid1Attached_ScrollChanged"
></Views:innerGridView>
<Views:innerGridView Grid.Row="3" Margin="2,0,2,2" DataContext="{Binding someCollection}"
x:Name="grid2Control"
ScrollViewer.ScrollChanged="Grid2Attached_ScrollChanged"
></Views:innerGridView>
then access that public ScrollViewer.SetHorizontalOffset(e.HorizontalOffset) method when another scrolling event occur.
Below code is in the OuterGridView.xaml.cs on one of the handler definition (
private void Grid1Attached_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e != null && !e.Handled)
{
if (e.HorizontalChange != 0.0)
{
grid2Control.Scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
e.Handled = true;
}
}
private void Grid2Attached_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e != null && !e.Handled)
{
if (e.HorizontalChange != 0.0)
{
grid1Control.Scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
e.Handled = true;
}
}
Also make sure any other scroll_changed event inside the inner grid (if any, for example if you define a TextBox with default scroller in one of the column data template) has its e.Handled set to true to prevent outer grid's handler processing it (this happened due to default bubbling behaviour of routedevents). Alternatively you can put additional if check on e.OriginalSource or e.Source to filter the scroll event you're intended to process.

Resources