WPF adaptative user interface - c#

Hi fellow programmers,
I'm working on a WPF software that uses a Canvas to display and move graphic objects.
When the user clic on an object, I need to display a panel with the selected object's properties.
These properties are different for each object, one can have a displayed text, another a background color or a scale value.
What is the best way to program this ?
I have 9 objects type, I'm searching for something more elegant than creating my controls in panels and switch betwenn then for every graphic object type.
Thank you for your help.
Edit - to show design code :
The dock panel for generated Wpf controls to display properties.
<DockPanel x:Name="pnlProperties" Width="200" Grid.Column="2" Background="red">
<Grid x:Name="GridProperties" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Margin="0,2,0,25" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="55"/>
<RowDefinition Height="55"/>
<RowDefinition Height="55"/>
<RowDefinition Height="55"/>
<RowDefinition Height="95"/>
<RowDefinition Height="95"/>
<RowDefinition Height="55"/>
<RowDefinition Height="55"/>
<RowDefinition Height="55"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ***** Label ***** -->
<Label x:Name="lblLabel1" Content="test Prop" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="0" FontSize="16"/>
<Label x:Name="lblLabel2" Content=" Prop 2" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="1" Grid.Column="0" FontSize="16"/>
<Label x:Name="lblLabel3" Content=" Prop 3" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="2" Grid.Column="0" FontSize="16"/>
<Label x:Name="lblLabel4" Content=" Prop 4" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="3" Grid.Column="0" FontSize="16"/>
</Grid>
</Grid>
</DockPanel>
The Canvas that displays the MovableObject (userControl) of each graphic objects :
<UserControl x:Class="DashEditor.Views.ScreenView"
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"
xmlns:local="clr-namespace:DashEditor.Views">
<Canvas x:Name="ObjectsCanvas" HorizontalAlignment="Center" VerticalAlignment="Center" Width="800" Height="480" AllowDrop="True" Background="Black" PreviewMouseLeftButtonDown="ObjectsCanvas_PreviewMouseLeftButtonDown" >
<Image x:Name="imgFond" Stretch="Fill" Source="/DashEditor;component/assets/FondXAP.png" Width="800" Height="480"/>
</Canvas>
One of the graphic object class :
[StructLayout(LayoutKind.Sequential)]
[XmlRoot("XapLabel")]
public class XapLabel : IXapGraphicObject
{
private int _id;
public int Id
{
get
{
return _id;
}
set
{
_id = value;
}
}
[XmlIgnore]
private MovableObject _Control;
[XmlIgnore]
public MovableObject Control
{
get
{
return _Control;
}
set
{
_Control = value;
}
}
private Point _pos;
public Point Pos
{
get
{
return _pos;
}
set
{
_pos = value;
}
}
public IXapGraphicObject getXapParent(MovableObject Control)
{
return this;
}
public ObjectType Type
{
get
{
return ObjectType.Label;
}
}
public XapLabel()
{
}
public void ConnectToMoveEvent()
{
_Control.OnObjectTranslating += _Control_OnObjectTranslating;
}
private void _Control_OnObjectTranslating(Vector displacement)
{
Pos = Pos + displacement;
}
}

I've done something similar to this, if you are familiar with MVVM:-
For the canvas, I used an ItemsControl bound to an ObservableCollection of your "graphic objects", to which you'll be adding objects that you want to appear on the canvas. You'll also need to change the ItemsControl's panel template to a Canvas:-
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="800" Height="480" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Your "graphic object" classes need to expose double properties (say "X" and "Y"), to control the object's position on the canvas.
Next, create a XAML DataTemplate for each of these classes, to define their visual appearance. The data template should include the following bindings:
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
For the property grid, rather than write your own, look at the free Xceed Toolkit community edition (here), which has a very good PropertyGrid control. You bind its SelectedObject property to the selected object, but read the documentation - there are plenty of decent features.
(If you are using MVVM then remember to change your classes to implement INotifyPropertyChanged, and raise the PropertyChanged event in the setters).
For the drag and drop functionality, you should just be able to set the selected object's X and Y values within the mouse move event.
Not a full solution I know, but will hopefully point you in the right direction.

Related

Programmatically adjusting the number of rows in a grid with C#

I'm currently teaching myself XAML/C# and writing a calendar application. Right now it's creating a grid and then applying user control elements to the grid. It is correctly building my calendar but, instead of defining the number of rows in XAML, I want to be able to set the number via C# dynamically. Some months use more or less weeks (March needs 5 but April needs 6). I'm wondering how to do that or if I should be using a different control than grid.
This is what the UI looks like.
XAML code
<UserControl x:Class="CMS.Control.MonthView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Background="AliceBlue">
<Image x:Name="MonthPrev" Source="/Images/Previous.png" Height="24" Margin="16,0,6,0" MouseLeftButtonUp="MonthPrev_MouseLeftButtonUp"/>
<Image x:Name="MonthNext" Source="/Images/Next.png" Height="24" Margin="6,0,16,0" MouseLeftButtonUp="MonthNext_MouseLeftButtonUp"/>
<Label x:Name="DateLabel" Content="January 2017" FontSize="16" FontFamily="Bold" VerticalAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="1" Background="AliceBlue">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
<ColumnDefinition MinWidth="60" Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Sunday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="1" Content="Monday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="2" Content="Tuesday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="3" Content="Wednesday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="4" Content="Thursday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="5" Content="Friday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
<Label Grid.Column="6" Content="Saturday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
</Grid>
<Grid x:Name="WeekRowGrid" Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
</Grid>
</Grid>
</UserControl>
C# Code
namespace CMS.Control
{
/// <summary>
/// Interaction logic for MonthView.xaml
/// </summary>
public partial class MonthView : UserControl
{
private DateTime _DispayDate;
public MonthView()
{
InitializeComponent();
_DispayDate = DateTime.Now;
DrawMonth();
}
//Generates the
private void DrawMonth()
{
DateTime FirstDayOfMonth = new DateTime(_DispayDate.Year, _DispayDate.Month, 1);
int DisplayFrontPadding = (int)FirstDayOfMonth.DayOfWeek; // # of days that need to be displayed before the 1st of the month
int DaysInDisplayMonth = DateTime.DaysInMonth(_DispayDate.Year, _DispayDate.Month);
int DaysInDisplay = DisplayFrontPadding + DaysInDisplayMonth;
DaysInDisplay += 7 - DaysInDisplay%7; // Rounds up the displayed days to a multiple of 7
DateLabel.Content = _DispayDate.ToString("MMMM") + " " + _DispayDate.Year;
for (int i = 0; i<DaysInDisplay; i++)
{
DateTime DisplayDay = FirstDayOfMonth.AddDays(i - DisplayFrontPadding);
DayBox DB = DayBox.GetDay(); // DayBox factory
DB.DrawDay(DisplayDay);
Grid.SetRow(DB, i / 7);
WeekRowGrid.Children.Add(DB);
Grid.SetColumn(DB, i%7);
}
}
//Generates a calendar for the previous month on button click.
private void MonthPrev_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_DispayDate = _DispayDate.AddMonths(-1);
DrawMonth();
}
//Generates a calendar for the next month on button click.
private void MonthNext_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_DispayDate = _DispayDate.AddMonths(1);
DrawMonth();
}
}
}
I know I should really be using MVVM but I'm still wrapping my brain around programming in MVVM pattern and want to get this working. I'll probably refactor it once I'm more comfortable with it.
I just wanted to finish this project
Understood. The thing is, the basic idea behind MVVM isn't really all that hard, and if you embrace it, you likely will finish the project faster, than if you continue to try to hard-code all your UI. I can't guarantee that, of course. But I've been through the same thing, and I can tell you that you can spend a lot of time fighting WPF trying to configure the UI in code-behind explicitly.
Without a good Minimal, Complete, and Verifiable code example to start with, it wasn't practical for me to replicate exactly your user interface. But here is a simple code example that shows the basic approach you might take to use MVVM to build the UI you want…
First, it's helpful to have a base class that implements INotifyPropertyChanged for you. It simplifies the view model boilerplate a lot:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
_OnPropertyChanged(propertyName);
}
}
protected virtual void _OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then, we'll need view models. In this UI, there are two basic components: the overall view, and the individual days of the month. So, I made a view model for each:
class DateViewModel : NotifyPropertyChangedBase
{
private int _dayNumber;
private bool _isCurrent;
public int DayNumber
{
get { return _dayNumber; }
set { _UpdateField(ref _dayNumber, value); }
}
public bool IsCurrent
{
get { return _isCurrent; }
set { _UpdateField(ref _isCurrent, value); }
}
}
and…
class MonthViewViewModel : NotifyPropertyChangedBase
{
private readonly ObservableCollection<DateViewModel> _dates = new ObservableCollection<DateViewModel>();
private DateTime _selectedDate;
public DateTime SelectedDate
{
get { return _selectedDate; }
set { _UpdateField(ref _selectedDate, value); }
}
public IReadOnlyCollection<DateViewModel> Dates
{
get { return _dates; }
}
protected override void _OnPropertyChanged(string propertyName)
{
base._OnPropertyChanged(propertyName);
switch (propertyName)
{
case nameof(SelectedDate):
_UpdateDates();
break;
}
}
private void _UpdateDates()
{
_dates.Clear();
DateTime firstDayOfMonth = new DateTime(SelectedDate.Year, SelectedDate.Month, 1),
firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);
int previousMonthDates = (int)firstDayOfMonth.DayOfWeek; // assumes Sunday-start week
int daysInView = previousMonthDates + DateTime.DaysInMonth(SelectedDate.Year, SelectedDate.Month);
// round up to nearest week multiple
daysInView = ((daysInView - 1) / 7 + 1) * 7;
DateTime previousMonth = firstDayOfMonth.AddDays(-previousMonthDates);
for (DateTime date = previousMonth; date < firstDayOfNextMonth; date = date.AddDays(1))
{
_dates.Add(new DateViewModel { DayNumber = date.Day, IsCurrent = date == SelectedDate.Date });
}
for (int i = 1; _dates.Count < daysInView; i++)
{
_dates.Add(new DateViewModel { DayNumber = i, IsCurrent = false });
}
}
}
As you can see, so far there's been no mention of UI, and yet already all the logic exists to build a month's worth of dates. The UI part, the XAML, will have no idea that you are doing anything related with months or dates. The closest it gets is a hard-coded invariant, i.e. the number of days in a week which are used to control the number of columns in the UniformGrid that will display your data.
The XAML looks like this:
<Window x:Class="TestSO43147585CalendarMonthView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="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:l="clr-namespace:TestSO43147585CalendarMonthView"
xmlns:s="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
SizeToContent="Height"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MonthViewViewModel SelectedDate="{x:Static s:DateTime.Today}"/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type l:MonthViewViewModel}">
<ItemsControl ItemsSource="{Binding Dates}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True" Columns="7"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
<DataTemplate DataType="{x:Type l:DateViewModel}">
<Border BorderBrush="Black" BorderThickness="0, 0, 1, 0">
<StackPanel>
<TextBlock Text="{Binding DayNumber}">
<TextBlock.Style>
<p:Style TargetType="TextBlock">
<Setter Property="Background" Value="LightBlue"/>
<p:Style.Triggers>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</TextBlock.Style>
</TextBlock>
<Grid Height="{Binding ActualWidth, RelativeSource={x:Static RelativeSource.Self}}"/>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding}" VerticalAlignment="Top"/>
</Grid>
</Window>
The XAML does three things:
It declares a MonthViewViewModel object to be used as the DataContext for the Window. An object in the visual tree, i.e. children of the Window, will inherit their parent's context if they have none of their own.
It declares data templates for the two view models. These tell WPF how it should visually represent the data. A view model contains the data that you want to represent, and this data is referenced in the template via {Binding...} syntax. In many cases (e.g. text, numbers, enum values), you can just bind directly and the default conversion will do what you want (as is the case above). If not, you can implement your own IValueConverter and incorporate that in the binding.
It provides a place for the MonthViewViewModel to be displayed, by declaring a ContentControl, where the content of that control is bound simply to the current data context (a {Binding} expression without a path will bind to the source, and the default source is the current data context).
In the context of the ContentControl, as well as the individual items being displayed in the ItemsControl, WPF will search for the template that is appropriate for the data object defined for that context, and will automatically populate your visual tree, binding to the necessary properties, according to that object.
There are a number of advantages to this approach, the primary ones being that you can describe your UI instead of having to code it, and that you maintain the OOP principle of "separation of concerns", which is key in reducing the mental work-load involved by allowing you to focus on one thing at a time, instead of having to deal with UI and data logic together.
A couple of side-notes regarding the XAML above:
You might notice that I have added the p: XML namespace and used it for the Style element. This is only work around a Stack Overflow bug, in which the Style element by itself confuses the XML formatter and prevents the element and its children from being formatted correctly. The XAML will compile fine like this, but it's not actually necessary in real code. In your regular XAML, you can safely omit it.
I included a feature your code didn't, just for the purpose of illustration. That is, the current date is highlighted in yellow. The technique shown here is very useful, as it allows you to customize the appearance of an item in a single template, based on property values of the view model. But there is a little trap: in WPF, if you explicitly set an element's property via the attribute syntax, e.g. something like <TextBlock Text="{Binding DayNumber}" Background="LightBlue">, then that syntax will take precedence over any <Setter...> elements in a style. You have to remember to set the default value of any property that you intend to set via a trigger, in its own <Setter...> in the style as well (as shown above).
You could create a method that adjust the number of rows automatically based on the number of weeks (rows). This method always remove all the rows and then add the correct number of rows you need.
private void AdjustRowDefinitions(int numberOfWeeks)
{
WeekRowGrid.RowDefinitions.Clear();
for (int i = 0; i < numberOfWeeks; i++)
{
RowDefinition rowDef = new RowDefinition();
rowDef.Height = new GridLength(1, GridUnitType.Star); //this sets the height of the row to *
WeekRowGrid.RowDefinitions.Add(rowDef);
}
}

WPF C# Get array position of grid button from Click Event

I have a window that I'm basically building ghetto minesweeper in. I have a grid that I feed a jagged array into, set up so that it will adapt to any change in the size of the array (no hard set values or rows/columns). Over top of that, I have a grid of blank buttons that simply adapts in size to the grid below.
When you click a button, it hides revealing the value under it, and I need some way to return the position of the button clicked, so I can compare against the original jagged array to find out whether not the item was a bomb. (this would also help me for doing a fill action on empty tiles). But given how I have it set up, the Grid.GetRow or Grid.GetColumn just return 0's.
Does anyone know how I can get the array position (preferably row and column) from the setup that I have?
XAML Below, the C# click events follow it.
<Window x:Class="MinesweeperWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Minesweeper" SizeToContent="WidthAndHeight" Height="Auto" Width="Auto">
<Window.Resources>
<DataTemplate x:Key="Buttons_Template">
<Button Content=""
Height="20"
Width="20"
Margin="0,0,0,0"
Visibility="Visible"
Click="ButtonClick"
MouseRightButtonUp="RightClick"/>
</DataTemplate>
<DataTemplate x:Key="DataTemplate_2">
<TextBlock Text="{Binding}"
Height="20"
Width="20"
Margin="0,0,0,0"
FontFamily="Rockwell"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="5"/>
</DataTemplate>
<DataTemplate x:Key="DataTemplate_1">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplate_2}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
<DataTemplate x:Key="Buttons_Template2">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource Buttons_Template}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<TextBlock x:Name="RemainingMines" HorizontalAlignment="Left" />
<TextBlock x:Name="Difficulty" HorizontalAlignment="Center" />
<TextBlock x:Name="Timer" HorizontalAlignment="Right" />
</Grid>
<Grid Name="ResetButton" Grid.Row="1">
<Button Name="Reset" Content="Reset"/>
</Grid>
<Grid Name="GridBoard" ShowGridLines="True" Grid.Row="2">
<ItemsControl x:Name="GridItems" ItemTemplate="{DynamicResource DataTemplate_1}"/>
</Grid>
<Grid Name="ButtonsBoard" ShowGridLines="True" Grid.Row="2">
<ItemsControl x:Name="ButtonItems" ItemTemplate="{DynamicResource Buttons_Template2}"/>
</Grid>
</Grid>
</Window>
Click Methods in C#
private void ButtonClick(object sender, RoutedEventArgs e)
{
int col = Grid.GetColumn((Button)sender); //this just returns 0
int row = Grid.GetRow((Button)sender); //this just returns 0
Button source = e.Source as Button;
source.Visibility = Visibility.Hidden;
Console.WriteLine("L: {0} x {1}", col, row); //prints L: 0 x 0
}
private void RightClick(object sender, RoutedEventArgs e)
{
int col = Grid.GetColumn((Button)sender); //this just returns 0
int row = Grid.GetRow((Button)sender); //this just returns 0
Button source = e.Source as Button;
if(source.Content.Equals(""))
{
source.Content = "\u2691";
}
else
{
source.Content = "";
}
Console.WriteLine("R: {0} x {1}", col, row); //prints R: 0 x 0
}
Any help would be appreciated on this.
You need to use appropriate control for buttons to host. In your xaml you are using items control inside Grid. But Items control do not have row and column index. Thats why you are not able to get the index.
Use UniformGrid or some relevant control.
You can refer this article
https://stackoverflow.com/a/13588066/5273015
Will help you a lot in other assignments as well

Invalidate Thumb position when IsDirectionReversed changes

I'm using a slider control in a C# WPF application and want to offer the ability for users to invert the control on the fly.
I've already established that the slider control has a built in IsDirectionReversed property which works well for changing the direction of the control (effectively swapping its minimum and maximum values to opposite ends of the control). However, when the property is changed on the fly, the thumb position remains where it is (visually implying an incorrect value) but its value remains the same (as I would expect).
(After inverting the control, clicking anywhere on the track causes the thumb to update to either +1 or -1 of its current value but repositions the thumb into its correct visual position)
Can anyone suggest a way of forcing the thumb to update its position when the inverted property is changed?
XAML
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="139" Width="303">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<Slider Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Name="SliderControl" HorizontalAlignment="Left" VerticalAlignment="Top" Width="276" IsDirectionReversed="{Binding Inverted}" Maximum="100" Minimum="1" SmallChange="1"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="Value:" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding ElementName=SliderControl, Path=Value}" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="Inverted:" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TextBlock Grid.Row="3" Grid.Column="2" Text="{Binding ElementName=SliderControl, Path=IsDirectionReversed}" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Button Grid.Row="4" Grid.Column="2" Content="Flip!" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
</Grid>
</Window>
C#
public partial class MainWindow : Window, INotifyPropertyChanged
{
public bool mSliderInverted = false;
public bool Inverted
{
get
{
return mSliderInverted;
}
}
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
mSliderInverted = !mSliderInverted;
OnPropertyChanged("Inverted");
}
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, e);
}
protected void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
It seems flipping the IsDirectionInversed property doesn't cause the control to rerender, I'd say this is a bug in WPF (Bug in WPF!? No way!).
By trial and error, I discovered that if you invalidate the PART_Track part of the slider (The element that contains the RepeatButtons and the Thumb), it magically switches it around and everything works the way it should!
So if you just modify your Button_Click handler to look like this, everything should work just fine. :-)
private void Button_Click(object sender, RoutedEventArgs e)
{
mSliderInverted = !mSliderInverted;
OnPropertyChanged("Inverted");
// Retrieve the Track from the Slider control
var track = SliderControl.Template.FindName("PART_Track", SliderControl) as Track;
// Force it to rerender
track.InvalidateVisual();
}

Unable to see my controls in MainPage.g.cs of my MainPage.xaml

I have a simple question about XAML.
I created a grid element inside another grid and added a textblock and a web browser in it. However, I am unable to access it in MainPage.xaml.cs using their name (e.g. this.LastName) doesn't work. On further debugging, I saw that they are not declared in MainPage.g.cs. Since MainPage.g.cs is auto defined, I don't know how to fix it. Can someone help please? Below is my C# and XAML code. Thanks!
========================================================
public partial class MainPage : Microsoft.Phone.Controls.PhoneApplicationPage {
internal System.Windows.Controls.Grid LayoutRoot;
internal System.Windows.Controls.Grid ContentPanel;
internal Microsoft.Phone.Controls.LongListSelector MainLongListSelector;
internal System.Windows.Controls.Image RefreshIcon;
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent() {
if (_contentLoaded) {
return;
}
_contentLoaded = true;
System.Windows.Application.LoadComponent(this, new System.Uri("/Suod;component/MainPage.xaml", System.UriKind.Relative));
this.LayoutRoot = ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot")));
this.ContentPanel = ((System.Windows.Controls.Grid)(this.FindName("ContentPanel")));
this.MainLongListSelector = ((Microsoft.Phone.Controls.LongListSelector)(this.FindName("MainLongListSelector")));
this.RefreshIcon = ((System.Windows.Controls.Image)(this.FindName("RefreshIcon")));
}
}
========================================================
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<phone:LongListSelector x:Name="MainLongListSelector" Margin="-12,-97,0,97" ItemsSource="{Binding Items}">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel Margin="12,100,0,45">
<Grid x:Name="CompanyContentGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="LastName" Text="{Binding Name}" TextWrapping="Wrap" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
<phone:WebBrowser Grid.Row="5" Height="400" Name="WebBrowser" Margin="12,-6,24,0" FontFamily="Portable User Interface"/>
</Grid>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
</Grid>
<Grid Grid.Row="2" >
<Image Source="/Assets/Refresh.png" Name="RefreshIcon" Width="80" Height="80" Tap="Image_Tap"/>
</Grid>
I think the problem is that your textblock i too deep in xaml code. I think Data Binding should be enough in this situation but there is a way to go around this problem. You have to subscribe this element to some event(like Loaded) and save sender to some property in code behind:
<TextBlock Loaded="LastName_Loaded" Grid.Row="0" Name="LastName" Text="{Binding Name}" TextWrapping="Wrap" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
public TextBlock lastName;
private void LastName_Loaded(object sender, RoutedEventArgs e)
{
lastName= (TextBlock)sender;
}
Now you are able to access this element in code behind.

windows phone 8: User control cannot be disposed when it was removed from its parent panel?

I have a simple application page with a content panel,and I add a usercontrol in the content panel. When I click a button to remove it, the userControl's destructor didn't excute. Why?
Here's my main page:
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Content="AddOrRemove" Click="Button_Click"/>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
</Grid>
</Grid>
the button click event is:
private void Button_Click(object sender, RoutedEventArgs e)
{
if (this.ContentPanel.Children.Count > 0)
{
this.ContentPanel.Children.Clear();
return;
}
page = new PromptPage();
this.ContentPanel.Children.Add(page);
}
PromptPage.xaml:
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
<StackPanel>
<TextBlock Text="balabalaabcdefghijklmnop1"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop2"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop3"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop4"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop5"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop6"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop7"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop8"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop9"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop10"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop11"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop12"></TextBlock>
<TextBlock Text="balabalaabcdefghijklmnop13"></TextBlock>
</StackPanel>
</Grid>
PromptPage.xaml.cs:
public PromptPage()
{
InitializeComponent();
}
~PromptPage()
{
System.Diagnostics.Debug.WriteLine("disposed!");
}
From your code, I can say of course it won't be disposed because your MainPage still holds the reference to page. Try set it null after remove:
page = null;
But it isn't enough to make the control disposed immediately. You must wait for the GC process or directly call it at some point later:
GC.Collect();

Categories