How to block a wpf UI to avoid illegal states? - c#

Lets say I have 3 or more Slider and each slider can have a value from 0 to 100.
However I want that the sum of all slider values is <= 100. In case I have 4 slider everyones max value would be 25.
Every slider has a binding to a double variable and every time the user uses a slider (tick frequency 0.1) I calculate the sum and set other slider back or if necessary set the same slider back, so that the sum is <= 100.
The problem is, that the calculation needs a decent amount of time and in the meantime the user can set illegal values. I would like to solve this by blocking the UI until the calculation is over. Basically the opposite of the desired responsiveness.
Other ideas and suggestions to solve the slider thing are welcome.
slider binding
public BindingList<WLCToolParameter> WLCParameter
{
get { return _toolParameter; }
set { _toolParameter = value; }
}
should be instant - not really :(
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MCDA.Entity;
using MCDA.Extensions;
namespace MCDA.Model
{
class ProportionalDistributionStrategy : IWeightDistributionStrategy
{
public void Distribute<T>(IList<T> listOfToolParameter) where T : class, IToolParameter
{
if (listOfToolParameter.Count == 0)
return;
IToolParameter lastWeightChangedToolParameter = lastWeightChangedToolParameter = listOfToolParameter[0].LastWeightChangedToolParameter;
double sumOfAllWeights = listOfToolParameter.Sum(t =>t.Weight);
//we have to rescale
if (sumOfAllWeights > 100)
{
double overrun = sumOfAllWeights - 100;
//how much do we have without the locked and the last changed?
double availableSpace = listOfToolParameter.Where(t => t.IsLocked == false && t != lastWeightChangedToolParameter).Sum(t => t.Weight);
//we have enough by taking from the non locked
if (availableSpace > overrun)
{
//lets remove proportional
double sumOfChangeableWeights = listOfToolParameter.Where(t => t.IsLocked == false && t != lastWeightChangedToolParameter).Sum(t => t.Weight);
//in case we have only one element that is suitable we can directly remove all from this one
if (listOfToolParameter.Where(t => t.IsLocked == false && t.Weight > 0 && t != lastWeightChangedToolParameter).Count() == 1)
{
listOfToolParameter.Where(t => t.IsLocked == false && t.Weight > 0 && t != lastWeightChangedToolParameter).ForEach(t => t.Weight = t.Weight - overrun);
return;
}
listOfToolParameter.Where(t => t.IsLocked == false && t.Weight > 0 && t != lastWeightChangedToolParameter).ForEach(t => t.Weight = t.Weight - (sumOfChangeableWeights / (sumOfChangeableWeights - t.Weight)) * overrun);
}
//we have to resize also the latest change, but we try to keep as much as possible of the latest change
else
{
//lets set them to zero
listOfToolParameter.Where(t => t.IsLocked == false && t != lastWeightChangedToolParameter).ForEach(t => t.Weight = 0);
//how much are we still over?
double stillOver = listOfToolParameter.Sum(t => t.Weight) - 100;
//and cut from the last changed
listOfToolParameter.Where(t => t == lastWeightChangedToolParameter).ForEach(t => t.Weight -= stillOver);
}
}
}
}
}

It looks like you are not making use of data binding. Here is a simple example - just add your calculation logic to the calculating method. The UI will update itself. Note this is a crude example. I am not sure I would implement it this way. Also be careful of using decimals in your numbers. If you use this with foreign languages / regional settings with a comma as the decimal separator - it will error out.
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<Slider Margin="10" Value="{Binding Path=Value1}" />
<TextBlock Text="{Binding Path=Value1}" />
<Slider Margin="10" Value="{Binding Path=Value2}" />
<TextBlock Text="{Binding Path=Value2}" />
<Slider Margin="10" Value="{Binding Path=Value3}" />
<TextBlock Text="{Binding Path=Value3}" />
</StackPanel>
</Grid>
</Window>
Code Behind (MVVM approach this would be in your View Model)
namespace WpfApplication3
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private double _value1;
public double Value1
{
get { return _value1; }
set
{
if(value != _value1)
{
_value1 = value;
DoMyCalculations(_value1, _value2, _value3);
NotifyPropertChanged("Value1");
}
}
}
private double _value2;
public double Value2
{
get { return _value2; }
set
{
if (value != _value2)
{
_value2 = value;
DoMyCalculations(_value1, _value2, _value3);
NotifyPropertChanged("Value2");
}
}
}
private double _value3;
public double Value3
{
get { return _value3; }
set
{
if (value != _value3)
{
_value3 = value;
DoMyCalculations(_value1, _value2, _value3);
NotifyPropertChanged("Value3");
}
}
}
private bool isCalculating = false;
private void DoMyCalculations(double value1, double value2, double value3)
{
if (isCalculating)
return;
isCalculating = true;
// Perform logic to reset here
isCalculating = false;
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notify of Property Changed event
/// </summary>
/// <param name="propertyName"></param>
public void NotifyPropertChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

Related

Avalonia: How to animate points in path using code

I'm trying to figure out how to do animations in Avalonia.
I have a path with 4 linesegements and I want to animate each point to a new position. In WPF I have done it like this:
public void AnimatePoints(PointCollection pts, TimeSpan timespan, bool randomized = true, Action onFinished = null)
{
Points = PointCollection.Parse(PathString);
//PathFigure needs an animation too (for the start point), otherwise the first point always stays in one place
var pfa = new PointAnimation(pts[0], timespan);
if (onFinished != null)
{
pfa.Completed += (sender, args) => onFinished();
}
PathFigure.BeginAnimation(PathFigure.StartPointProperty, pfa);
for (int i = 0; i < pts.Count; i++)
{
var pa = new PointAnimation(pts[i], timespan);
if (randomized)
{
LineSegments[i].BeginAnimation(LineSegment.PointProperty, pa);
}
else
{
LineSegments[i].BeginAnimation(LineSegment.PointProperty, pa);
}
}
}
How can I do the same in Avalonia using code? I've tried with a PathTransition but neither PathFigure nor LineSegments are animateable.
I don't think there is built-in animator, but in Avalonia you can do custom animators like that:
public class MorphAnimator : Animator<Geometry>
{
public override Geometry Interpolate(double progress, Geometry oldValue, Geometry newValue)
{
var clone = (oldValue as PathGeometry).ClonePathGeometry();
Morph.To(clone, newValue as PathGeometry, progress);
return clone;
}
}
and register
Animation.RegisterAnimator<MorphAnimator>(prop => typeof(Geometry).IsAssignableFrom(prop.PropertyType));
Example code: https://github.com/wieslawsoltes/MorphingDemo
You can also do custom animators from Xaml:
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="clr-namespace:RenderDemo.Pages"
x:Class="RenderDemo.Pages.CustomAnimatorPage"
MaxWidth="600">
<Grid>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock.Styles>
<Style Selector="TextBlock">
<Style.Animations>
<Animation Duration="0:0:1" IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Text" Value="" Animation.Animator="{x:Type pages:CustomStringAnimator}"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Text" Value="0123456789" Animation.Animator="{x:Type pages:CustomStringAnimator}"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</TextBlock.Styles>
</TextBlock>
</Grid>
</UserControl>
Animator:
using Avalonia.Animation.Animators;
namespace RenderDemo.Pages
{
public class CustomStringAnimator : Animator<string>
{
public override string Interpolate(double progress, string oldValue, string newValue)
{
if (newValue.Length == 0) return "";
var step = 1.0 / newValue.Length;
var length = (int)(progress / step);
var result = newValue.Substring(0, length + 1);
return result;
}
}
}

How to scroll DataGrid inside ScrollView in right way?

I think it is a pretty common situation, but I can't find a solution that I need.
My case is, I have a DataGrid inside ScrollViewer. DataGrid has such params
MinHeight="350"
MaxHeight="350"
So, it is means that I have a dataGrid with constant height inside ScrollViewer. And DataGrid can contain (example) 300 items, so it has a scroll and ScrollViewer also has a scroll.
What I need is when user put a mouse on DataGrid and scroll down, so content inside DataGrid scrolling up to the last item and if came to last item it means that now we should get scroll event to ScrollViewer and continuous scroll whole page...
I found a few solutions on SO like this two
Mouse scroll not working in a scroll viewer with a wpf datagrid and additional UI elements
ScrollViewer mouse wheel not working
I checked an answers, but them doesn't fit to my problem. Because what I get is - when user put mouse on DataGrid and try scroll, so DataGrid did not get scroll event ever, just ScrollViewer get scroll event...
How to fix it?
XAML
<Window x:Class="so_wpf_test_1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:so_wpf_test_1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Border Padding="10" Background="Yellow">
<ScrollViewer x:Name="sv">
<StackPanel Orientation="Vertical">
<DataGrid x:Name="dg" MinHeight="350" MaxHeight="350" ScrollViewer.ScrollChanged="dg_ScrollChanged">
</DataGrid>
<Button>A button that doesn't do anything</Button>
<DataGrid x:Name="dg2" MinHeight="350" MaxHeight="350" ScrollViewer.ScrollChanged="dg_ScrollChanged">
</DataGrid>
</StackPanel>
</ScrollViewer>
</Border>
</Window>
Code-behind
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace so_wpf_test_1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var items = new ObservableCollection<Item>();
for (int i = 1; i <= 50; ++i)
{
items.Add(new Item($"test {i}", $"abcd {i}", $"dcba {i}"));
}
dg.ItemsSource = items;
dg2.ItemsSource = items;
dg.PreviewMouseWheel += Dg_PreviewMouseWheel;
dg2.PreviewMouseWheel += Dg_PreviewMouseWheel;
}
private void Dg_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var m_dg = sender as DataGrid;
var scroll = GetScrollViewer(m_dg);
// if scrolling to bottom and beyond...
if (e.Delta < 0 && scroll.VerticalOffset == scroll.ScrollableHeight)
{
sv.ScrollToVerticalOffset(sv.VerticalOffset - e.Delta);
}
// if scrolling to top and beyond...
else if (e.Delta > 0 && scroll.VerticalOffset == 0)
{
sv.ScrollToVerticalOffset(sv.VerticalOffset - e.Delta);
}
else
{
// nothing special: scroll the dg if it is the case (WPF does this automatically)
}
int idx = 0;
// if scrolling down
if (e.Delta < 0)
{
// see the sketch
idx = dictLastVisible[m_dg] + 1;
Debug.WriteLine($"FROM {(m_dg == dg ? 1 : 2)} DOWN {idx}");
}
// if scrolling up
else if (e.Delta > 0)
{
// see the sketch
idx = dictFirstVisible[m_dg] - 1;
Debug.WriteLine($"FROM {(m_dg == dg ? 1 : 2)} UP {idx}");
}
// if the index does not represent nothing
if (idx != 0)
{
// transform index to be 0-based
--idx;
// scroll that row into view
m_dg.ScrollIntoView(m_dg.Items[idx]);
}
}
/// <summary>
/// Source: https://stackoverflow.com/a/41136328/258462
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static ScrollViewer GetScrollViewer(UIElement element)
{
if (element == null) return null;
ScrollViewer retour = null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element) && retour == null; i++)
{
if (VisualTreeHelper.GetChild(element, i) is ScrollViewer)
{
retour = (ScrollViewer)(VisualTreeHelper.GetChild(element, i));
}
else
{
retour = GetScrollViewer(VisualTreeHelper.GetChild(element, i) as UIElement);
}
}
return retour;
}
Dictionary<DataGrid, int> dictLastVisible = new Dictionary<DataGrid, int>();
Dictionary<DataGrid, int> dictFirstVisible = new Dictionary<DataGrid, int>();
private void dg_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var dg = (DataGrid)sender;
int idxb = -1, idxe = -1; // b = beginning, e = end; both invalid initially
// from the first row towards the last row
for (int i = 0; i < dg.Items.Count; i++)
{
var v = (DataGridRow)dg.ItemContainerGenerator.ContainerFromItem(dg.Items[i]);
if (v != null)
{
idxb = i + 1; // compute the beginning row in the viewport
break;
}
}
// from the beginning row towards the last row
for (int i = idxb + 1; i < dg.Items.Count; i++)
{
var v = (DataGridRow)dg.ItemContainerGenerator.ContainerFromItem(dg.Items[i]);
if (v == null)
{
idxe = i - 1 + 1; // compute the end row in the viewport
break;
}
}
// store the two indices in two dictionaries
dictFirstVisible[dg] = idxb;
dictLastVisible[dg] = idxe;
}
}
public class Item
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public Item(string a, string b, string c)
{
A = a;
B = b;
C = c;
}
}
}
Possibly helping sketch
If the code does not run or has bugs, or the comments are not clear enough, please post a comment. I hope you will be able to extend the code for a dynamic number of DataGrids if you have this requirement.

Drag and drop elements inside a cavas that is inside a ScrollViewer on touch screen

I have a ScrollViewer inside which I have some expanders and of the Expander's content has a Canvas. I am drawing two rectangles on the Canvas which are Thumb controls that can be dragged around. Problem is, whenever I press on the rectangles and try to drag, it somehow assumes that I want to scroll the entire page and the Drag never happens. How can I solve this problem inside the ScrollViewer.
There is a property called CanBeScrollAnchorwhich I saw on MSDN for UWP that says the property is used for (Gets or sets a value that indicates whether the UIElement can be a candidate for scroll anchoring.). But setting that to false doesn't help. (I am adding the WPF tag because the concept is the same). Please help
Edit: This issue occurs only on the touch screen.
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<uwpControls:Expander>
<!--Some Content-->
</uwpControls:Expander>
<uwpControls:Expander HeaderStyle="{StaticResource ToggleButtonFullWidthHeader}"
Background="{StaticResource ListViewItemBackgroundBrush}" Margin="0,20,0,0" HorizontalAlignment="Stretch" Header="DragDrop">
<Grid x:Name="gridBarImagePanel" Grid.Row="4" BorderBrush="Black" BorderThickness="2">
<uwpControls:ImageEx x:Name="BarCodeImage" IsCacheEnabled="True" Source="..\SampleImage\demo.png"
Stretch="UniformToFill" />
<Canvas x:Name="cnvBarCodeImage" Canvas.ZIndex="100" VerticalAlignment="Stretch">
</Canvas>
</Grid>
</uwpControls:Expander>
</StackPanel>
</ScrollViewer>
In the code behind, I am calling this method to draw two Thumb controls on the Canvas dynamically.
public sealed partial class SomeControl:Page
{
event Action<Thumb, double, double> ChangeDimensions;
public void OnSomeButtonClick(object sender,EventArgs e)
{
//TEST(Remove this)
dimensions.Add(new Dimensions()
{
Height = 80,
Width = 150,
left = 250,
Top = 40,
BorderColor = BrushCollection[0],
MaxLeftPossible = null,
MaxTopPossible = null,//280
MaxRightPossible = null,
MaxBottomPossible = null,
});
dimensions.Add(new Dimensions()
{
Height = 80,
Width = 150,
left = 250,
Top = 150,
BorderColor = BrushCollection[1],
MaxLeftPossible = null,
MaxTopPossible = null,//280
MaxRightPossible = null,
MaxBottomPossible = null,
});
//CreateUIShapes(SelectedRowItem.WindowLocations.Count, dimensions);
CreateUIShapes(2, dimensions);
}
private void CreateUIShapes(int numberOfWindows, List<Dimensions> dimensions)
{
Thumb th = null;
for (int i = 0; i < numberOfWindows; i++)
{
th = new Thumb();
th.Name = string.Format("{0}{1}", "Thumb", i + 1);
var item = dimensions[i];
th.Width = item.Width;
th.Height = item.Height;
th.Foreground = new SolidColorBrush(Windows.UI.Colors.Transparent);
th.BorderBrush = item.BorderColor;
th.BorderThickness = new Thickness(3);
th.DragDelta += (sender, e) => Th_DragDelta(sender, e, dimensions);
th.DragCompleted += (sender, e) => Th_DragCompleted(sender, e);
Canvas.SetLeft(th, CheckAndGetItemToLeft(item.left, i));
Canvas.SetTop(th, CheckAndGetItemTotop(item.Top, i));
cnvBarCodeImage.Children.Add(th);
}
}
//This is the code to drag and drop.
private void Th_DragDelta(object sender, DragDeltaEventArgs e, List<Dimensions> LimitedWindowMovements)
{
var SelectedDraggableThumb = sender as Thumb;
double left = (Canvas.GetLeft(SelectedDraggableThumb) >= 0) ? Canvas.GetLeft(SelectedDraggableThumb) : 0;
double top = (Canvas.GetTop(SelectedDraggableThumb) >= 0) ? Canvas.GetTop(SelectedDraggableThumb) : 0;
if (ChkDualMode.IsChecked == true)
{
var OtherDraggableThumbs = cnvBarCodeImage.Children.OfType<Thumb>().Where(x => x != SelectedDraggableThumb);
var maxTopSelected = LimitedWindowMovements[0].MaxTopPossible == null ? 1 : LimitedWindowMovements[0].MaxTopPossible;
var maxBottomSelected = LimitedWindowMovements[0].MaxBottomPossible == null ? BarCodeImage.ActualHeight - SelectedDraggableThumb.ActualHeight : LimitedWindowMovements[0].MaxBottomPossible;
var maxLeftSelected = LimitedWindowMovements[0].MaxLeftPossible == null ? 0 : LimitedWindowMovements[0].MaxLeftPossible;
var maxRightSelected = LimitedWindowMovements[0].MaxRightPossible == null ? BarCodeImage.ActualWidth - SelectedDraggableThumb.Width : LimitedWindowMovements[0].MaxRightPossible;
//Boundry case for top->Current element.
//Don't allow further up.
if (top < maxTopSelected) //Hardcoding for now. top < 1 original.
{
if (e.VerticalChange <= 1)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//Boundry case for bottom->Current element.
//Don't allow further down.
//Canvas.GetTop(SelectedDraggableThumb) >= BarCodeImage.ActualHeight - SelectedDraggableThumb.ActualHeight original.
else if (Canvas.GetTop(SelectedDraggableThumb) >= maxBottomSelected)
{
if (e.VerticalChange > 0)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//Boundry case for left->Current element.
//Don't allow further left.
//Canvas.GetLeft(SelectedDraggableThumb) <= 0 original.
else if (Canvas.GetLeft(SelectedDraggableThumb) <= maxLeftSelected)
{
if (e.HorizontalChange <= 1)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//Boundry case for left->Current element.
//Don't allow further right.
//Math.Round(Canvas.GetLeft(SelectedDraggableThumb), 1) >= BarCodeImage.ActualWidth - SelectedDraggableThumb.Width original.
else if (Math.Round(Canvas.GetLeft(SelectedDraggableThumb), 1) >= maxRightSelected)
{
if (e.HorizontalChange >= 0)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//There may be n number of windows.
foreach (var item in OtherDraggableThumbs)
{
//Boundry case for the other thumb moving up.
//Don't allow further up.
//var OtherTop = Canvas.GetTop(item); original
var OtherTop = Canvas.GetTop(item);
if (OtherTop < maxTopSelected)
{
if (e.VerticalChange <= 1)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//Boundry case for the other thumb moving bottom
//Don't allow further bottom.
//BarCodeImage.ActualHeight - SelectedDraggableThumb.ActualHeight original
if (Canvas.GetTop(item) >= maxBottomSelected)
{
if (e.VerticalChange > 0)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//Boundry case for the other thumb moving left.
//Don't allow further left
//Canvas.GetLeft(item) <= 0 original
if (Canvas.GetLeft(item) <= maxLeftSelected)
{
if (e.HorizontalChange <= 1)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
//Boundry case for the other thumb moving right.
//Don't allow further right
//Math.Round(Canvas.GetLeft(item), 1) >= BarCodeImage.ActualWidth - item.Width original
if (Math.Round(Canvas.GetLeft(item), 1) >= maxRightSelected)
{
if (e.HorizontalChange >= 0)
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
return;
}
}
if (ChangeDimensions == null)
{
ChangeDimensions += BarCodeServiceView_ChangeDimensions;
}
}
foreach (var item in OtherDraggableThumbs)
{
ChangeDimensions?.Invoke(item, e.HorizontalChange, e.VerticalChange);
}
}
left = (Canvas.GetLeft(SelectedDraggableThumb) > BarCodeImage.ActualWidth - SelectedDraggableThumb.ActualWidth) ? BarCodeImage.ActualWidth - SelectedDraggableThumb.ActualWidth : left;
top = (Canvas.GetTop(SelectedDraggableThumb) > BarCodeImage.ActualHeight - SelectedDraggableThumb.ActualHeight) ? BarCodeImage.ActualHeight - SelectedDraggableThumb.ActualHeight : top;
Canvas.SetLeft(SelectedDraggableThumb, left + e.HorizontalChange);
Canvas.SetTop(SelectedDraggableThumb, top + e.VerticalChange);
}
//When one thumb moves, I want to move the other in the same direction.
private void BarCodeServiceView_ChangeDimensions(Thumb sender, double arg1, double arg2)
{
if (sender != null)
{
//4025e8df
double left = (Canvas.GetLeft(sender) > 0) ? Canvas.GetLeft(sender) : 0;
double top = (Canvas.GetTop(sender) > 0) ? Canvas.GetTop(sender) : 0;
left = (Canvas.GetLeft(sender) > BarCodeImage.ActualWidth - sender.ActualWidth) ? BarCodeImage.ActualWidth - sender.ActualWidth : left;
top = (Canvas.GetTop(sender) > BarCodeImage.ActualHeight - sender.ActualHeight) ? BarCodeImage.ActualHeight - sender.ActualHeight : top;
Canvas.SetLeft(sender, left + arg1);
Canvas.SetTop(sender, top + arg2);
}
}
private void Th_DragCompleted(object sender, DragCompletedEventArgs e)
{
//Do something.
foreach (var item in cnvBarCodeImage.Children.OfType<Thumb>())
{
ChangeDimensions -= BarCodeServiceView_ChangeDimensions;
}
}
}
public class Dimensions
{
public double Height { get; set; }
public double Width { get; set; }
public double left { get; set; }
public double Top { get; set; }
public Windows.UI.Xaml.Media.Brush BorderColor { get; set; }
/// <summary>
/// If no restriction needed specify null
/// </summary>
public double? MaxTopPossible { get; set; }
/// <summary>
/// If no restriction needed specify null
/// </summary>
public double? MaxLeftPossible { get; set; }
/// <summary>
/// If no restriction needed specify null
/// </summary>
public double? MaxRightPossible { get; set; }
/// <summary>
/// If no restriction needed specify null
/// </summary>
public double? MaxBottomPossible { get; set; }
}

I have two WPF controls, but only one of them is refreshing when I refresh their data bindings

I have a BindableGrid control I created in WPF, and I instantiated two of them on my page:
<Viewbox Name="galaxyViewbox" Width="400" Height="400" DataContext="{x:Static models:Galaxy.Current}">
<local:BindableGrid x:Name="galaxyMap" ArraySource="{Binding StarSystemArray}" CellSize="16" BackgroundImage="/Images/Starfield.png">
<local:BindableGrid.ItemTemplate>
<DataTemplate>
<local:TileView ImagePaths="{Binding ImagePaths}"/>
</DataTemplate>
</local:BindableGrid.ItemTemplate>
</local:BindableGrid>
</Viewbox>
<Viewbox Name="starSystemViewbox" Stretch="Uniform" Grid.RowSpan="2" Grid.Column="1" DataContext="{x:Static models:StarSystem.Current}">
<Grid>
<local:BindableGrid x:Name="starSystemMap" ArraySource="{Binding SpaceObjectArray}" CellSize="64" BackgroundImage="/Images/Starfield.png">
<local:BindableGrid.ItemTemplate>
<DataTemplate>
<local:TileView ImagePaths="{Binding ImagePaths}"/>
</DataTemplate>
</local:BindableGrid.ItemTemplate>
</local:BindableGrid>
...
</Grid>
</Viewbox>
Both of them are displaying the appropriate images; however when the current star system occupied by the player ship in my game changes, only the starSystemMap control updates; the galaxyMap control doesn't update as well (it's supposed to show a ship icon on the currently occupied system). I don't see what's so different about how these controls are being bound; I'm calling this code every time the player ship moves to force them to refresh:
galaxyMap.GetBindingExpression(BindableGrid.ArraySourceProperty).UpdateTarget();
starSystemMap.GetBindingExpression(BindableGrid.ArraySourceProperty).UpdateTarget();
I know there's other code interacting with this; let me know if there's anything else you need to see to figure this out! Thanks!
edit: sharing more details, first BindableGrid.ArraySource:
/// <summary>
/// The array to bind to. Should be a 1D or 2D array.
/// </summary>
public Array ArraySource
{
get { return (Array)GetValue(ArraySourceProperty); }
set { SetValue(ArraySourceProperty, value); }
}
// Using a DependencyProperty as the backing store for ArraySource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ArraySourceProperty =
DependencyProperty.Register(nameof(ArraySource), typeof(Array), typeof(BindableGrid), new PropertyMetadata(null));
and BindableGrid.OnPropertyChanged:
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ArraySourceProperty || e.Property == ItemTemplateProperty || e.Property == CellSizeProperty || e.Property == BackgroundImageProperty)
{
if (ArraySource != null)
{
ColumnDefinitions.Clear();
RowDefinitions.Clear();
int width = ArraySource.GetLength(0);
int height = ArraySource.Rank == 1 ? 1 : ArraySource.GetLength(1);
for (var x = 0; x < width; x++)
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(CellSize) });
for (var y = 0; y < height; y++)
RowDefinitions.Add(new RowDefinition { Height = new GridLength(CellSize) });
if (ItemTemplate != null)
{
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
var item = ArraySource.Rank == 1 ? ArraySource.GetValue(x) : ArraySource.GetValue(x, y);
var box = Children.Cast<UIElement>().Where(b => (int)b.GetValue(RowProperty) == y && (int)b.GetValue(ColumnProperty) == x && (int)b.GetValue(RowSpanProperty) == 1 && (int)b.GetValue(ColumnSpanProperty) == 1).SingleOrDefault();
if (box == null)
{
box = (UIElement)ItemTemplate.LoadContent();
box.SetValue(ColumnProperty, x);
box.SetValue(RowProperty, y);
Children.Add(box);
}
if (item == Wormholes.Models.StarSystem.Current)
{
}
box.SetValue(DataContextProperty, item);
}
}
}
}
if (bg != null)
bg.Source = BackgroundImage;
else
bg = new Image { Source = BackgroundImage };
if (ColumnDefinitions.Count > 0 && RowDefinitions.Count > 0)
{
bg.SetValue(ColumnSpanProperty, ColumnDefinitions.Count);
bg.SetValue(RowSpanProperty, RowDefinitions.Count);
if (!Children.Contains(bg))
Children.Add(bg);
}
}
}
}
edit: and here's my ImagePaths property which is not being rebound to display the latest images for each star system:
public ObservableCollection<string> ImagePaths { get; set; } = new ObservableCollection<string>();
public void RefreshImagePaths()
{
ImagePaths.Clear();
ImagePaths.Add("Star");
if (Current == this)
{
foreach (var ip in PlayerShip.Instance.ImagePaths)
ImagePaths.Add(ip);
}
}
RefreshImagePaths is called on the star system you move out of and the one you move into when you change star systems.
edit: what's really interesting is that I call box.SetValue(DataContextProperty, item); when binding the BindableGrid, but the UI isn't updated for the galaxy map...
edit: here is my TileView.xaml.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wormholes.Views
{
/// <summary>
/// Interaction logic for TileView.xaml
/// </summary>
public partial class TileView : UserControl
{
public TileView()
{
InitializeComponent();
}
/// <summary>
/// Paths to images to display, from back to front Z-index.
/// </summary>
public ObservableCollection<string> ImagePaths
{
get { return (ObservableCollection<string>)GetValue(ImagePathsProperty); }
set { SetValue(ImagePathsProperty, value); }
}
// Using a DependencyProperty as the backing store for ImagePaths. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ImagePathsProperty =
DependencyProperty.Register(nameof(ImagePaths), typeof(ObservableCollection<string>), typeof(TileView), new PropertyMetadata(null));
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ImagePathsProperty)
{
grid.Children.Clear();
if (ImagePaths != null)
{
foreach (var ip in ImagePaths)
{
try
{
var img = new Image { Source = new BitmapImage(new Uri(System.IO.Path.GetFullPath(System.IO.Path.Combine("Images", ip + ".png")), UriKind.RelativeOrAbsolute)), Width = Width, Height = Height };
grid.Children.Add(img);
}
catch (FileNotFoundException ex)
{
Console.Error.WriteLine($"Could not find image: {ip}");
}
catch (DirectoryNotFoundException ex)
{
Console.Error.WriteLine($"Could not find image: {ip}");
}
}
}
}
}
}
}
The OnPropertyChanged method of my TileView was never getting called because the TileView's DataContext and ImagePaths never actually changed; only the items inside ImagePaths changed. Therefore I had to force a refresh in BindableGrid.OnPropertyChanged:
box.DataContext = null;
box.DataContext = item;
I'm sure there's a better way to do this, but for now, it works!

WPF numeric format

I have this XAML for acolumn into DataGrid
<DataGridTemplateColumn Header="% Deduccion Anticipo">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding NumPorcentajeAnticipo, Mode=TwoWay, StringFormat={}{0:00.}%}" Visibility="{Binding Merlin_ConceptosFacturacion.BitOtrosItms_Anticipos,Converter={StaticResource boolToVisibility}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding NumPorcentajeAnticipo, Mode=TwoWay,StringFormat={}{0:00.}%}" Visibility="{Binding Merlin_ConceptosFacturacion.BitOtrosItms_Anticipos,Converter={StaticResource boolToVisibility}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
The Stringformat applys as i expect, but muy problem is the user can fill it with any char alpha, number symbol, how can i do to prevent it, it is posible set an inputmask ?
I'm was trying with another StringFormats but any one of them work as I expect.
UPDATE: The column is currently binded to a Numeric property of my view model.
You can use the KeyDown event of a TextBox to intercept and filter out invalid values. You could even create your own derived TextBox and override OnKeyDown for a better encapsulated solution.
None of the built-in controls have the ability to specify an input mask. But, there are masked input controls out there on the internet for WPF. We are using the Telerik Rad Controls for WPF package and it has such a control. I use it in my application and it works very well.
you can achieve your requirement by using following methods.
Put the masked textbox in CellEditingTemplate and set the mask in that masked textbox.
Create the custom render based on your requirement and bind to the CellEditingTemplate.
After some research found this on another question:
Numeric Data Entry in WPF And #Brian Hinchey answer match with some of my needs.
Just add by myself some Culture validations for decimal numbers plus some editing and validation tools. Hope this help somebody else.
To use it:
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<controls:NumericTextBox DecimalPlaces="2" DecimalSeparator="."/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
If no decimal places or separator are provided, it take CultureInfo.CurrentCulture parms
Here is the final code:
public class NumericTextBox : TextBox
{
#region Formato
private string previousText = "";
private bool ApplyingFormat = false;
private CultureInfo _CI = new CultureInfo(CultureInfo.CurrentCulture.LCID,true);
public CultureInfo CI
{
get { return _CI; }
set { _CI = value; }
}
private int _DecimalPlaces = 0;
/// <summary>
/// Numero de plazas decimales
/// </summary>
public int DecimalPlaces
{
get { return _DecimalPlaces; }
set { _DecimalPlaces = value; _CI.NumberFormat.NumberDecimalDigits = value; }
}
public Decimal DecimalValue = 0;
private string _DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
public string DecimalSeparator
{
get { return _DecimalSeparator; }
set { _DecimalSeparator = value; _CI.NumberFormat.NumberDecimalSeparator = _DecimalSeparator; }
}
//public string DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
#endregion
public NumericTextBox()
{
HorizontalContentAlignment = HorizontalAlignment.Right;
DataObject.AddPastingHandler(this, OnPaste);
}
private void OnPaste(object sender, DataObjectPastingEventArgs dataObjectPastingEventArgs)
{
var isText = dataObjectPastingEventArgs.SourceDataObject.GetDataPresent(System.Windows.DataFormats.Text, true);
if (isText)
{
var text = dataObjectPastingEventArgs.SourceDataObject.GetData(DataFormats.Text) as string;
if (IsTextValid(text))
{
return;
}
}
dataObjectPastingEventArgs.CancelCommand();
}
private bool IsTextValid(string enteredText)
{
// If keyboard insert key is in toggled mode, and the actual insert point is Decimalseparator, we must avoid to overwrite it
if (SelectionStart == this.Text.IndexOf(DecimalSeparator)
& System.Windows.Input.Keyboard.GetKeyStates(System.Windows.Input.Key.Insert) == System.Windows.Input.KeyStates.Toggled)
{
SelectionStart += 1;
}
if (!enteredText.All(c => Char.IsNumber(c) || c == DecimalSeparator.ToCharArray()[0] || c == '-'))
{
return false;
}
//We only validation against unselected text since the selected text will be replaced by the entered text
var unselectedText = this.Text.Remove(SelectionStart, SelectionLength);
if ( enteredText == DecimalSeparator && unselectedText.Contains(DecimalSeparator))
{
// Before return false, must move cursor beside Decimal separator
SelectionStart = this.Text.IndexOf(DecimalSeparator) + 1;
return false;
}
if (enteredText == "-" && unselectedText.Length > 0)
{
return false;
}
return true;
}
private bool ApplyFormat(TextChangedEventArgs e)
{
if (!ApplyingFormat)
{
ApplyingFormat = true;
int SelectionStartActual = SelectionStart;
string FinallText = this.Text;
if (!FinallText.Contains(DecimalSeparator) & DecimalPlaces > 0)
{
FinallText = String.Format("{0}{1}{2}", this.Text, DecimalSeparator, new string('0', DecimalPlaces));
}
bool state = Decimal.TryParse(FinallText, NumberStyles.AllowCurrencySymbol | NumberStyles.AllowDecimalPoint | NumberStyles.AllowTrailingSign, _CI, out DecimalValue);
DecimalValue = Math.Round(DecimalValue, DecimalPlaces);
if (DecimalValue == 0)
{
FinallText = "";
}
else
{
if (FinallText != DecimalValue.ToString(_CI))
{
FinallText = DecimalValue.ToString(_CI);
}
}
if (FinallText != this.Text)
{
this.Text = FinallText;
SelectionStart = SelectionStartActual;
}
previousText = this.Text;
ApplyingFormat = false;
return state;
}
else
{
return true;
}
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
e.Handled = !ApplyFormat(e);
base.OnTextChanged(e);
}
protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
{
e.Handled = !IsTextValid(e.Text);
base.OnPreviewTextInput(e);
}
}

Categories