Easy Drag Drop implementation MVVM - c#

I am new to MVVM and I am currently trying to add the drag/drop feature to my application. The thing is I already developed the interface in the code-behind but I am trying now to re-write the code into MVVM as I am only at the beginning of the project.
Here is the context: the user will be able to add boxes (ToggleButton but it may change) to a grid, a bit like a chessboard. Below is the View Model I am working on:
<Page.Resources>
<Style TargetType="{x:Type local:AirportEditionPage}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Page}">
<!-- The page content-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ToolKitWidth, FallbackValue=50}" />
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="{Binding RightPanelWidth, FallbackValue=400}"/>
</Grid.ColumnDefinitions>
<!-- The airport grid where Steps and Links are displayed -->
<ScrollViewer Grid.ColumnSpan="4" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Viewbox Height="{Binding AirportGridHeight}" Width="{Binding AirportGridWidth}" RenderOptions.BitmapScalingMode="HighQuality">
<ItemsControl x:Name="ChessBoard" ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="{Binding CardQuantityRow}" Height="{Binding CardQuantityColumn}" Background="{StaticResource AirportGridBackground}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="1" Height="1">
<ToggleButton Style="{StaticResource StepCardContentStyle}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Pos.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Pos.Y}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Viewbox>
</ScrollViewer>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Page.Resources>
Items are basically from a class (child of INotifiedPropertyChanged) with a name, an icon and a position (Point).
Now, I am trying to make the user able to drag and drop the box (ToggleButton) within the grid wherever he/she wants. However, I am totally lost with Commands, AttachedProperties etc. I spent all the whole day on tutorials and tried drag/drop solutions but with my poor knowledge, I don't know how to apply all of this into my code.
On my code-behinded version of the code, it was easy. When the button is left-clicked, I say to a variable of the grid "hey, I am being dragged and dropped". While the user is moving, I changed the Item coordinates and when the user released the left button (left button up), the dragdrop_object variable comes null again.
In the frame of the MVVM, I am totally lost. Could you give me some tracks to help me trough ? I intended to give up with MVVM a lot of time, but I know that it is better to keep up even if every little feature takes litteraly hours for me to implement (it should decrease with time...).
Do not hesitate if you need further details to answer to my question.

I found the solution here : Move items in a canvas using MVVM and here : Combining ItemsControl with draggable items - Element.parent always null
To be precise, here is the code I added :
public class DragBehavior
{
public readonly TranslateTransform Transform = new TranslateTransform();
private static DragBehavior _instance = new DragBehavior();
public static DragBehavior Instance
{
get { return _instance; }
set { _instance = value; }
}
public static bool GetDrag(DependencyObject obj)
{
return (bool)obj.GetValue(IsDragProperty);
}
public static void SetDrag(DependencyObject obj, bool value)
{
obj.SetValue(IsDragProperty, value);
}
public static readonly DependencyProperty IsDragProperty =
DependencyProperty.RegisterAttached("Drag",
typeof(bool), typeof(DragBehavior),
new PropertyMetadata(false, OnDragChanged));
private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// ignoring error checking
var element = (UIElement)sender;
var isDrag = (bool)(e.NewValue);
Instance = new DragBehavior();
((UIElement)sender).RenderTransform = Instance.Transform;
if (isDrag)
{
element.MouseLeftButtonDown += Instance.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp += Instance.ElementOnMouseLeftButtonUp;
element.MouseMove += Instance.ElementOnMouseMove;
}
else
{
element.MouseLeftButtonDown -= Instance.ElementOnMouseLeftButtonDown;
element.MouseLeftButtonUp -= Instance.ElementOnMouseLeftButtonUp;
element.MouseMove -= Instance.ElementOnMouseMove;
}
}
private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
((UIElement)sender).CaptureMouse();
}
private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
((UIElement)sender).ReleaseMouseCapture();
}
private void ElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
FrameworkElement element = sender as FrameworkElement;
Canvas parent = element.FindAncestor<Canvas>();
var mousePos = mouseEventArgs.GetPosition(parent);
if (!((UIElement)sender).IsMouseCaptured) return;
if (mousePos.X < parent.Width && mousePos.Y < parent.Height && mousePos.X >= 0 && mousePos.Y >=0)
((sender as FrameworkElement).DataContext as Step).Pos = new System.Drawing.Point(Convert.ToInt32(Math.Floor(mousePos.X)), Convert.ToInt32((Math.Floor(mousePos.Y))));
}
}
And my DataTemplate is now:
<DataTemplate>
<ContentControl Height="1" Width="1" local:DragBehavior.Drag="True" Style="{StaticResource StepCardContentControl}"/>
</DataTemplate>
I added the FindAncestor static class in a dedicated file like this:
public static class FindAncestorHelper
{
public static T FindAncestor<T>(this DependencyObject obj)
where T : DependencyObject
{
DependencyObject tmp = VisualTreeHelper.GetParent(obj);
while (tmp != null && !(tmp is T))
{
tmp = VisualTreeHelper.GetParent(tmp);
}
return tmp as T;
}
}
(My items are now ContentControls).
As the items' positions within the canvas are directly managed with their Pos variable (Canvas.SetLeft and Canvas.SetTop based on Pos (Pos.X and Pos.Y) with Binding), I just update it according to the MousePosition within the Canvas.
Also, as suggested in a commentary, I will see if there is something better than the ScrollViewer and Viewbox I'm using.

Related

Itemsstackpanel disables scrolling

I have an Itemsstackpanel inside a Listview Control. I want to fire the PointerWheelChanged event whenever the User is near the edge of a page.
When i put the event on the itemsstackpanel is disables my ability to scroll by mouse wheel. If the event is on the Listview or Grid itself it only works as long as no Items are done loading in the Listview.
Is this intended behaviour or am i missing some important information?
I researched but found no lead to this problem or behaviour.
below is my XAML:
<Grid Background="Gray">
<ProgressRing x:Name="progress" IsActive="False" x:FieldModifier="public" Foreground="Black" Height="200" Width="200"/>
<ListView x:Name="ListViewControl" x:FieldModifier="public" Margin="10,10,10,10" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.ZoomMode="Enabled" ScrollViewer.VerticalScrollBarVisibility="Visible" DataFetchSize="10" IncrementalLoadingTrigger="Edge"
IncrementalLoadingThreshold="2" ShowsScrollingPlaceholders="True" BorderThickness="1" IsItemClickEnabled="False" SelectionMode="None" PointerEntered="ListViewControl_PointerEntered">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel CacheLength="0" Orientation="Vertical" Background="White" PointerWheelChanged="Grid_PointerWheelChanged"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
</Grid>
and the code behind(not done just trying to get it to work for now):
private void ItemsStackPanel_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
PointerPoint pointerPoint = e.GetCurrentPoint(ListViewControl);
float scrolledDistance = pointerPoint.Properties.MouseWheelDelta;
if (scrolledDistance >=120)
{
//Load Page above current Page at a certain mousewheel point
}
else if (scrolledDistance <= -120 )
{
//Load Page below current Page at certain mousewheel point
}
else
{
//do some other stuff
}
}
The default ControlTemplate of ListView contains a ScrollViewer control. PointerWheelChanged is a relatively low-level event that will be intercepted by ScrollViewer.
If you want to monitor the change of the scrolling distance, PointerWheelChanged is not a recommended event. You can listen to the ScrollViewer.ViewChanged event and use ScrollViewer.VerticalOffset to determine the vertical scrolling distance.
We can create a custom ListView to achieve this:
CustomListView.cs
public class CustomListView:ListView
{
private ScrollViewer _scrollViewer;
public event EventHandler<ScrollViewerViewChangedEventArgs> ViewChanged;
public CustomListView()
{
this.DefaultStyleKey = typeof(ListView);
}
protected override void OnApplyTemplate()
{
_scrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
if (_scrollViewer != null)
{
_scrollViewer.ViewChanged += CustomViewChange;
}
base.OnApplyTemplate();
}
private void CustomViewChange(object sender, ScrollViewerViewChangedEventArgs e)
{
ViewChanged?.Invoke(sender, e);
}
}
Usage
<controls:CustomListView x:Name="ListViewControl"
ViewChanged="ListViewControl_ViewChanged"
...>
<!--other content-->
</controls:CustomListView>
private void ListViewControl_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
double scrollHeight = scrollViewer.VerticalOffset;
if (scrollHeight > 120)
{
//Do Something...
}
else
{
//Do something...
}
}

User defined form, wpf binding controls position

I'm trying to let the user to design his own form and save the controls positions into a DB.
Right now I was able to allow the user to spawn new controls and move them around the form. What I dont know is how to get the position of the controls to my Datacontext. I was able to only bind the width etd...
I was hoping I could bind the canvas.left & canvas.top to datacontext but those are not updated on the renderTransform.
Any Ideas?Thanks for help.
Heres the form back code for moving the controls:
private Control _currentlyDragged;
private Point _currentlyDraggedMouseOffset;
private void Window_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (_currentlyDragged != null)
{
var mousePos = e.GetPosition(this);
var futurePos = e.GetPosition(BuildCanvas);
if (futurePos.X <= 0 || futurePos.Y <= 0 || futurePos.Y >= BuildCanvas.ActualHeight || futurePos.X >= BuildCanvas.ActualWidth)
return;
_currentlyDragged.RenderTransform = new TranslateTransform(mousePos.X - _currentlyDraggedMouseOffset.X, mousePos.Y - _currentlyDraggedMouseOffset.Y);
}
}
private void Window_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_currentlyDragged != null)
_currentlyDragged = null;
ReleaseMouseCapture();
}
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point pt = e.GetPosition((UIElement)sender);
_hitResultsList.Clear();
VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(MyHitTestResult), new PointHitTestParameters(pt));
if (!_hitResultsList.Where(h => h is Border && ((Border)h).Name == "BuildCanvas").Any())
return;
if (_hitResultsList.Count > 0)
{
foreach (DependencyObject d in _hitResultsList)
{
var parent = VisualTreeHelper.GetParent(d);
if (parent != null && (parent is Label || parent is TextBox))
{
CaptureMouse();
_currentlyDragged = parent as Control;
if (_currentlyDragged.RenderTransform is TranslateTransform)
{
_currentlyDraggedMouseOffset.X = e.GetPosition(this).X - ((TranslateTransform)_currentlyDragged.RenderTransform).X;
_currentlyDraggedMouseOffset.Y = e.GetPosition(this).Y - ((TranslateTransform)_currentlyDragged.RenderTransform).Y;
}
else
{
_currentlyDraggedMouseOffset.X = pt.X;
_currentlyDraggedMouseOffset.Y = pt.Y;
}
return;
}
}
}
_currentlyDragged = null;
}
private HitTestResultBehavior MyHitTestResult(HitTestResult result)
{
_hitResultsList.Add(result.VisualHit);
return HitTestResultBehavior.Continue;
}
Heres the ItemsControl:
<Border x:Name="BuildCanvas" Grid.Column="1" Grid.Row="0" Background="#fff4c9" CornerRadius="10">
<Grid>
<!-- Generated controls -->
<ItemsControl ItemsSource="{Binding TextBoxCollection}" Panel.ZIndex="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Padding="2" IsEnabled="False" Background="White"
Text="{Binding Name, Mode=OneWay}" Width="{Binding Width}">
</TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- -->
</Grid>
</Border>
Rendertransform is a bad idea. Each textbox is in a container.
You should bind the Canvas.Top and Canvas.Left of the itemcontainer. Something like:
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding XviewModelProperty}" />
<Setter Property="Canvas.Top" Value="{Binding YviewModelProperty}" />
</Style>
</ItemsControl.ItemContainerStyle>
Cast the datacontext of your textbox to whatever your viewmodel type is and set XviewModelProperty and YviewModelProperty.
Ensure they raise property changed when set.

Drag and Drop in UWP in list of bank accounts

I have a Universal Windows Application for a local bank, I'm working on a money transfer view, and they need to transfer money from account to account using the Drag and Drop feature in UWP applications.
I've made the animation part, but I need help after I drop the list item to the "Account To" list.
I'll attach a screenshot to make it clear.
As you see in the picture, I need to drag one item from the "From Account" list and drop it on only one item on "To Account" list. How can I achieve this ?
I've created a small sample which shows drag-drop between two ListViews filled with some Accounts. I will skip the implementation of UserControls - the Page xaml looks like this:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="200"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<ListView Header="Source" Margin="10" Grid.Row="0" CanDragItems="True" ItemsSource="{x:Bind Accounts}" SelectionMode="None">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<controls:AccountControl CanDrag="True" DragStarting="AccountControl_DragStarting"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView Header="Targets" Margin="10" Grid.Row="1" ItemsSource="{x:Bind Accounts}" SelectionMode="None">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<controls:AccountControl AllowDrop="True" DragEnter="AccountControl_DragEnter" Drop="AccountControl_Drop"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
As you can see there is a Source list in which the control is firing an event when it's being dragged.
private void AccountControl_DragStarting(UIElement sender, DragStartingEventArgs args)
{
if ((sender as AccountControl)?.DataContext is Account account)
{
args.AllowedOperations = DataPackageOperation.Link;
args.Data.SetData(accountId, account.Id);
}
}
The Account class apart from name and balance has a Guid identifier so I can use it to pass information which source account has been used in transfer method.
The items in second list (Targets) accepts only drop operation and for this purpose fire two events:
private void AccountControl_DragEnter(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Link;
e.DragUIOverride.Caption = "Transfer";
}
private async void AccountControl_Drop(object sender, DragEventArgs e)
{
if ((e.OriginalSource as AccountControl)?.DataContext is Account targetAccount)
if (await (e.DataView.GetDataAsync(accountId)) is Guid sourceAccountId)
{
var sourceAccount = Accounts.First(x => x.Id == sourceAccountId);
sourceAccount.Balance -= 1000;
targetAccount.Balance += 1000;
}
}
The first one sets accepted operation and some information for the user. The second one 'transfers' some money from one account to the second.
Everything looks like this:
Some more help you can find at MS directly, other article and in MS samples repository.
I am not fully satisfied with the "solutions" which I will provide. They are much likely very far away from the ideal implementations, but ...
The XAML code which I created to try to replicate as easily, but also consistently your object, consisted in a group of draggable Rectangles inside a StackPanel Control, plus another StackPanel Control where the items could be dragged into.
<Grid>
<Grid.Resources>
<Style TargetType="Rectangle">
<Setter Property="Width" Value="300"/>
<Setter Property="Height" Value="300"/>
<Setter Property="CanDrag" Value="True"/>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Name="StackPanelRectangles" Grid.Row="0" Orientation="Horizontal">
<Rectangle x:Name="RedRect" Fill="Red" DragStarting="Rectangle_DragStarting"/>
<Rectangle x:Name="GreenRect" Fill="Green" DragStarting="Rectangle_DragStarting"/>
<Rectangle x:Name="BlueRect" Fill="Blue" DragStarting="Rectangle_DragStarting"/>
</StackPanel>
<StackPanel Name="StackPanelDropArea" Background="Azure" AllowDrop="True"
DragOver="StackPanel_DragOver" Drop="StackPanel_Drop"
Grid.Row="2" Orientation="Horizontal"
HorizontalAlignment="Center">
<TextBlock>Drop anywhere in this area area</TextBlock>
</StackPanel>
</Grid>
1st Solution:
I routed every DragStarting event of the multiple Rectangles to the same EventHandler. In this EventHandler, we have access to the UIElement which is being dragged, so with an exposed property of type UIElement in your Page class, and you can simply clone the necessary properties for when you need to drop it, like this:
UIElement dragItem;
private void Rectangle_DragStarting(UIElement sender, DragStartingEventArgs args)
{
dataPackage.RequestedOperation = DataPackageOperation.Copy;
dragItem = sender;
}
Then when the item is dropped EventHandler is called, I have simply add it onto my DropArea.
private void StackPanel_Drop(object sender, DragEventArgs e)
{
Rectangle newElement = new Rectangle();
newElement.Width = (dragItem as Rectangle).Width;
newElement.Height = (dragItem as Rectangle).Height;
newElement.Fill = (dragItem as Rectangle).Fill;
StackPanelDropArea.Children.Add(newElement);
}
You cannot add your new Control by setting to reference the object being dragged, since there are properties such as the respective Parent which will thrown an exception when you try to add the Control to a different container.
2nd Solution:
I was extremely focused on on utilizing the DataPackage object, and one of its supported default formats, but I don't think any of them can actually hold data of an Object, such as our UIElement.
But each DataPackage instance supports a set of properties, which corresponds a Dictionary. We can set the Dictionary to hold UIElement in there, as long as we specify a key to reference that same object later on.
private void Rectangle_DragStarting(UIElement sender, DragStartingEventArgs args)
{
dataPackage.RequestedOperation = DataPackageOperation.Copy;
args.Data.Properties.Add("myRectangle", sender);
}
In the drop Event Handler, you can obtain the UIElement, like such:
private async void StackPanel_Drop(object sender, DragEventArgs e)
{
Rectangle element = e.DataView.Properties["myRectangle"] as Rectangle;
......
......
}
3rd Solution:
This solution used the method SetText(String) exposed by DataPackage, to hold the value of the Name property of the UIElement being dragged.
private void Rectangle_DragStarting(UIElement sender, DragStartingEventArgs args)
{
dataPackage = new DataPackage();
dataPackage.RequestedOperation = DataPackageOperation.Copy;
Rectangle rectangle = sender as Rectangle;
dataPackage.SetText(rectangle.Name);
Clipboard.SetContent(dataPackage);
}
By knowing the value of the Name property of the UIElement which is being dragged, looked for it, by using the VisualTreeHelper Class, like this:
private async void StackPanel_Drop(object sender, DragEventArgs e)
{
DataPackageView dataPackageView = Clipboard.GetContent();
if (dataPackageView.Contains(StandardDataFormats.Text))
{
draggedObject = await dataPackageView.GetTextAsync();
}
// Dragged objects come from another one of our Parent's Children
DependencyObject parent = VisualTreeHelper.GetParent(StackPanelDropArea);
int count = VisualTreeHelper.GetChildrenCount(parent);
for(int i=0; i< count; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if(child.GetType().Equals(typeof(StackPanel)))
{
StackPanel currentStackPanel = child as StackPanel;
if(currentStackPanel.Name == "StackPanelRectangles")
{
int numberOfRectangles = VisualTreeHelper.GetChildrenCount(currentStackPanel);
for(int j=0; j<numberOfRectangles; j++)
{
if(VisualTreeHelper.GetChild(currentStackPanel,j).GetType().Equals(typeof(Rectangle)))
{
Rectangle currentRectangle = VisualTreeHelper.GetChild(currentStackPanel, j) as Rectangle;
if (draggedObject != string.Empty && currentRectangle.Name.Equals(draggedObject))
{
Rectangle newRectangle = new Rectangle();
newRectangle.Width = currentRectangle.Width;
newRectangle.Height = currentRectangle.Height;
newRectangle.Fill = currentRectangle.Fill;
StackPanelDropArea.Children.Add(newRectangle);
}
}
}
}
}
} */
}
Result:
I usually try tackling this several times before giving up and using a third party library. The one I typically use is:
https://github.com/punker76/gong-wpf-dragdrop
You may subscribe to PointerPressed event in your DataTemplate and extract all the things you need.
XAML:
<DataTemplate x:Name="DataTemplate">
<Grid Background="Transparent" PointerPressed="Grid_OnPointerPressed"/>
</DataTemplate>
Code:
private void Grid_OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
//your FrameworkElement
var frameworkElement = sender as FrameworkElement;
//global position of your element
var itemPosition = frameworkElement.TransformToVisual(Window.Current.Content).TransformPoint(new Point(0, 0)).ToVector2();
//your data
var selectedItemData = frameworkElement.DataContext as ItemData;
}
Save your data, use UWP Drag'n'Drop. On drop load your data.

ZoomExtents method call works different than activating ZoomExtents through gesture

I've been working on a small 3D preview window in a MVVM style application... The view is created then its data context is set. Therefore it seems that ZoomExtentsWhenLoaded="True" doesn't seem to help do what I need. I need something like, ZoomExtentsWhenDataContextChanges.
Interestingly, I've found that if I use a mouse gesture like the one defined below, I can physically click on the HelixViewport3D and it will perform a ZoomExtents.
HelixViewport3D.ZoomExtentsGesture = new MouseGesture(MouseAction.LeftDoubleClick);
However, if do something like this...
HelixViewport3D.DataContextChanged += (o, e) => ResetCamera();
private void ResetCamera()
{
var dc = HelixViewport3D.DataContext as WellSurveyPlot3DViewModel;
HelixViewport3D.ResetCamera();
HelixViewport3D.Camera = dc.PerspectiveCamera;
HelixViewport3D.ZoomExtents();
}
The viewport does zoom, it just doesn't center itself, like it does when activating ZoomExtents when using the mouse gesture.
I tried ResetCamera, and several other things... What is the standard way of dealing with keeping a viewport around and swapping out the DataContext instead of creating a new one each time?
I fixed this with an attached property. I read through the HelixViewport3D source code and got this idea, after noticing how the camera works. It seems an update to the default camera through a property binding doesn't really do anything after the control is initialized.
public static class HelixViewport3DZoomExtent
{
private static readonly Type OwnerType = typeof(HelixViewport3DZoomExtent);
public static readonly DependencyProperty ZoomExtentsOnUpdateProperty = DependencyProperty.RegisterAttached("ZoomExtentsOnUpdate", typeof(bool), OwnerType, new PropertyMetadata(false, OnDataContextChanged));
public static bool GetZoomExtentsOnUpdate(DependencyObject obj)
{
return (bool)obj.GetValue(ZoomExtentsOnUpdateProperty);
}
public static void SetZoomExtentsOnUpdate(DependencyObject obj, bool value)
{
obj.SetValue(ZoomExtentsOnUpdateProperty, value);
}
private static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var viewport = d as HelixViewport3D;
if (viewport == null) return;
if (viewport.DataContext == null) return;
viewport.Camera = viewport.DefaultCamera;
viewport.ZoomExtents();
}
}
Here is the Xaml
<Border BorderBrush="Black" BorderThickness="1">
<Grid>
<h:HelixViewport3D Name="HelixViewport3D"
PanGesture="LeftClick"
DataContext="{Binding PreviewPlot, UpdateSourceTrigger=PropertyChanged}"
DefaultCamera="{Binding PerspectiveCamera, UpdateSourceTrigger=PropertyChanged}"
services:HelixViewport3DZoomExtent.ZoomExtentsOnUpdate="{Binding RelativeSource={RelativeSource AncestorType={x:Type views:WellSurveyPlot3DPreview}},
Path=DataContext.PreviewUpdatedReZoom, UpdateSourceTrigger=PropertyChanged}">
<h:SunLight/>
<h:TubeVisual3D Path="{Binding TubePath}" Diameter="75" ThetaDiv="12" IsPathClosed="False" Fill="LightGray"/>
<h:GridLinesVisual3D Width="{Binding GridLength}" Length="{Binding GridLength}" MajorDistance="{Binding MajorGridLines}" Thickness="25"
MinorDistance="{Binding MajorGridLines, UpdateSourceTrigger=PropertyChanged}" LengthDirection="1,0,0" Normal="0,0,1"
Center="{Binding BottomPlaneCenter,UpdateSourceTrigger=PropertyChanged}" Fill="Red" />
<h:GridLinesVisual3D Width="{Binding GridLength}" Length="{Binding GridLength, UpdateSourceTrigger=PropertyChanged}" LengthDirection="0,0,1" Normal="1,0,0" Thickness="25"
MajorDistance="{Binding MajorGridLines}" MinorDistance="{Binding MajorGridLines}"
Center="{Binding BackLeftPlaneCenter, UpdateSourceTrigger=PropertyChanged}" Fill="Blue" />
<h:GridLinesVisual3D Width="{Binding GridLength}" Length="{Binding GridLength, UpdateSourceTrigger=PropertyChanged}" LengthDirection="1,0,0" Normal="0,1,0" Thickness="25"
MajorDistance="{Binding MajorGridLines}" MinorDistance="{Binding MajorGridLines}"
Center="{Binding BackRightPlaneCenter,UpdateSourceTrigger=PropertyChanged}" Fill="Green" />
</h:HelixViewport3D>
<Button Content="Open Well Viewer" HorizontalAlignment="Left" VerticalAlignment="Top" Command="{Binding OpenWindowCmd}"/>
</Grid>
</Border>
In my view model I have to toggle my PreviewUpdateReZoom property.
private void LoadSurveyPoints(List<WellSurveyPointCalculated> surveyPoints)
{
_coordinatesCalculator = _calcGlobalCoordsFactory.Create(surveyPoints);
_wellXyzCoordinates = _coordinatesCalculator.PlotGlobalCoordinates(100).ToList();
PreviewPlot = WellSurveyPlot3DViewModel();
PreviewUpdatedReZoom = false;//Toggle true false to send property changed and get attached property to fire.
PreviewUpdatedReZoom = true;
}
This now works such that every new item drawn into the viewport has the correct camera settings and zooms to extents...

How can I find a style trigger-embedded element by name in WPF?

First, the heart of the question: If an element is assigned as the Content of a ContentControl via a style trigger, I can't seem to find it by name.
Now, for more detail: I have a panel that varies greatly in its layout and functionality based on its data context, which is a bug from a bug depot. When that bug is null, it is a search form, when it is non-null, it is a simple viewer for properties of that bug. The XAML then look something like:
<ContentControl DataContext="...">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="Content">
<Setter.Value>
...
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter Property="Content">
<StackPanel>
<TextBox Name="Waldo"/>
<Button .../>
</StackPanel>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
When the user clicks the button that sits alongside the text box, I get a callback in the code behind. From that point I'd like to be able to access various properties of the text box. The question is, where's Waldo? :)
In the code behind I have tried a few variants of the following, all with little success:
this.FindName("Waldo"); // Always returns null
I've seen a lot of discussion on this topic as it relates to templates but not as it relates to setting content directly with triggers. Maybe it's because I am violating all sorts of best practices by doing this :)
Thank you!
If an element is assigned as the Content of a ContentControl via a style trigger, I can't seem to find it by name.
If you needed to access to the Content before trigger occurs, it would most likely not possible. In this situation, the main thing to get access after the DataTrigger occurs.
I am violating all sorts of best practices by doing this
Maybe it's not the right way to work with the Сontrol in WPF, the more that you still need access to dynamic content, which can later be changed. But in any case, there are two ways to work with the Сontrol - it's like now and in the MVVM style. MVVM style is best suited for large and less complex applications with different business logic. If in your case for easy application, in this situation, I do not see anything wrong with that. In addition to doing a project in MVVM style need from scratch, combine conventional method and the correct method is not a good way.
I created a small example to demonstrate access controls for a given situation. There is a property that corresponds to the type of Content, the default is Init. If you assigns null for this property, the dynamic Content is loaded.
That's how I get access to TextBox:
private void GetAccessToTextBox_Click(object sender, RoutedEventArgs e)
{
TextBox MyTextBox = null;
StackPanel panel = MainContentControl.Content as StackPanel;
foreach (object child in panel.Children)
{
if (child is TextBox)
{
MyTextBox = child as TextBox;
}
}
if (MyTextBox != null)
{
MyTextBox.Background = Brushes.Gainsboro;
MyTextBox.Height = 100;
MyTextBox.Text = "Got access to me!";
}
}
Below it's a full example:
XAML
<Window x:Class="AccessToElementInContentControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:AccessToElementInContentControl"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<this:TestData />
</Window.DataContext>
<Window.Resources>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Content">
<Setter.Value>
<Label Content="InitContent"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=TypeContent}" Value="{x:Null}">
<Setter Property="Content">
<Setter.Value>
<StackPanel Name="NullStackPanel">
<TextBox Name="Waldo" Text="DynamicText" />
<Button Width="100" Height="30" Content="DynamicButton" />
</StackPanel>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ContentControl Name="MainContentControl" />
<Button Name="SetContentType"
Width="100"
Height="30"
HorizontalAlignment="Left"
Content="SetContentType"
Click="SetContentType_Click" />
<Button Name="GetAccessToButton"
Width="110"
Height="30"
HorizontalAlignment="Right"
Content="GetAccessToTextBox"
Click="GetAccessToTextBox_Click" />
</Grid>
</Window>
Code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void SetContentType_Click(object sender, RoutedEventArgs e)
{
TestData test = this.DataContext as TestData;
test.TypeContent = null;
}
private void GetAccessToTextBox_Click(object sender, RoutedEventArgs e)
{
TextBox MyTextBox = null;
StackPanel panel = MainContentControl.Content as StackPanel;
foreach (object child in panel.Children)
{
if (child is TextBox)
{
MyTextBox = child as TextBox;
}
}
if (MyTextBox != null)
{
MyTextBox.Background = Brushes.Gainsboro;
MyTextBox.Height = 100;
MyTextBox.Text = "Got access to me!";
}
}
}
public class TestData : NotificationObject
{
private string _typeContent = "Init";
public string TypeContent
{
get
{
return _typeContent;
}
set
{
_typeContent = value;
NotifyPropertyChanged("TypeContent");
}
}
}
public class NotificationObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Categories