Trying to understand pattern to use with BeginInvoke() - c#

I have two TextBlock’s that I am positioning consecutively on a Canvas. The first case works fine:
TextBlock text1 = new TextBlock();
text1.Text = "Not ";
text1.FontSize = 18;
Canvas.SetTop(text1, 20);
Canvas.SetLeft(text1, 20);
canvas.Children.Add(text1);
TextBlock text2 = new TextBlock();
text2.Text = "bad!";
text2.FontSize = 18;
Canvas.SetTop(text2, 20);
canvas.Dispatcher.BeginInvoke(DispatcherPriority.Background,
new DispatcherOperationCallback(delegate(Object state)
{
Canvas.SetLeft(text2, 20 + text1.ActualWidth);
return null;
}
), null);
canvas.Children.Add(text2);
Result:
However, the second case, which does not use BeginInvoke(), fails:
TextBlock text1 = new TextBlock();
text1.Text = "Not ";
text1.FontSize = 18;
Canvas.SetTop(text1, 20);
Canvas.SetLeft(text1, 20);
canvas.Children.Add(text1);
TextBlock text2 = new TextBlock();
text2.Text = "bad!";
text2.FontSize = 18;
Canvas.SetTop(text2, 20);
Canvas.SetLeft(text2, 20 + text1.ActualWidth); // ActualWidth is zero.
canvas.Children.Add(text2);
Result:
Now, I know that in the second case, the WPF rendering has not happened yet. My question is simply this: What is the preferred pattern to use in such a case where I need to know the actual coordinate values for UI controls which are only available after rendering has taken place?
(e.g. Is the approach, where BeginInvoke() is used, a good solution? Should the entire code be enclosed in a giant BeginInvoke()?)

To answer your question:
Dispatcher.BeginInvoke() queues the operation in the Dispatcher's "pending jobs" queue. This allows it to be able to process the addition of the first UI element, and run the Layout and Render passes before continuing to execute your code.
Therefore, when your code is run, the size of the first TextBlock has already been calculated, and you can get it.
Again, I don't know what you're attempting to do, but creating UI elements in code is usually a sign of a poor design. WPF is not winforms and the WPF ways are completely different from the horrible hacks required to do anything in winforms.
Edit:
This is my approach using a WrapPanel and some RenderTransform:
<Window x:Class="MiscSamples.MovingWords"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MovingWords" Height="300" Width="300">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Thumb DragDelta="Thumb_DragDelta" Margin="2">
<Thumb.Template>
<ControlTemplate>
<TextBlock Text="{Binding Text}"
FontSize="{Binding FontSize}"
Foreground="{Binding Color}"/>
</ControlTemplate>
</Thumb.Template>
<Thumb.RenderTransform>
<TranslateTransform X="{Binding OffsetX}" Y="{Binding OffsetY}"/>
</Thumb.RenderTransform>
</Thumb>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code Behind:
public partial class MovingWords : Window
{
public ObservableCollection<MovingWordModel> Words { get; set; }
public MovingWords()
{
InitializeComponent();
Words = new ObservableCollection<MovingWordModel>
{
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "Hello!!"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "This"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "is"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "the"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "Power"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "of"},
new MovingWordModel() {Color = "Blue", FontSize = 18, Text = "WPF"},
};
DataContext = Words;
}
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
var thumb = sender as Thumb;
if (thumb == null)
return;
var word = thumb.DataContext as MovingWordModel;
if (word == null)
return;
word.OffsetX += e.HorizontalChange;
word.OffsetY += e.VerticalChange;
}
}
Data Model:
public class MovingWordModel:PropertyChangedBase
{
public string Text { get; set; }
public int FontSize { get; set; }
public string Color { get; set; }
private double _offsetX;
public Double OffsetX
{
get { return _offsetX; }
set
{
_offsetX = value;
OnPropertyChanged("OffsetX");
}
}
private double _offsetY;
public double OffsetY
{
get { return _offsetY; }
set
{
_offsetY = value;
OnPropertyChanged("OffsetY");
}
}
}
PropertyChangedBase:
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
You can click and drag the words to move them around.
Notice that the values from the dragging will be stored in the OffsetX and OffsetY properties. The only problem with this approach is that you somewhat lose the Resolution Independence, because the offset values will actually move the words from their default position (which is determined by the WrapPanel, therefore they're subject to change depending on the size of the WrapPanel itself).

Related

LiveCharts Cartesian Mapping and Configuration with custom Labels

I have plotted the Cartesian chart using X and Y values, to mimic step chart (x values are not consistent). Is it possible to add custom x Labels for each step changed in Y value? Or similar labels like in a bar chart.
var values = new ChartValues<Point>();
Loop:
var point = new Point() { X = entry.DailyXFrom, Y = entry.YValue };
values.Add(point);
point = new Point() { X = entry.DailyXTo, Y = entry.YValue };
values.Add(point);
tempLabels.Add(entry.DailyVolume.ToString());
seriesCollectionDaily = new SeriesCollection
{
new LineSeries
{
Configuration = new CartesianMapper<Point>()
.X(point => point.X)
.Y(point => point.Y)
,
Fill = Brushes.Transparent,
Title = "Series",
Values = values,
PointGeometry = null,
LineSmoothness = 0
}
};
XAxis.Separator.IsEnabled = true;
XAxis.Labels = tempLabels.ToArray();
chart.DataContext = this;
<lvc:CartesianChart Name="chart" Grid.Row="2" Grid.ColumnSpan="2" Series="{Binding seriesCollectionDaily }" >
<lvc:CartesianChart.AxisX >
<lvc:Axis Name="XAxis" Title="" LabelsRotation="0" Foreground="Black" >
<lvc:Axis.Separator>
<lvc:Separator Name="LabelSeparator"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
</lvc:CartesianChart>
What I got:
This is what I'm trying to achieve. Is it possible?
You can add sections to your chart axis.
To draw vertical sections you have to define a SectionsCollection of AxisSection items and bind it to Axis.Sections of the x-axis.
By setting AxisSection.Value you can define the position on the axis. Use AxisSection.SectionWidth to define the width of the section. The default is 1, which draws a simple stroke when AxisSection.StrokeThickness is set.
The following example uses StepLineSeries to plot a step chart.
To give an example, it shows two vertical lines placed at the corresponding x-value: a simple line (SectionWidth = 1) and a section where SectionWidth > 1 (e.g., to highlight a range):
Data Model
public class DataModel : INotifyPropertyChanged
{
public DataModel()
{
this.SeriesCollection = new SeriesCollection
{
new StepLineSeries()
{
Configuration = new CartesianMapper<ObservablePoint>()
.X(point => point.X)
.Y(point => point.Y)
.Stroke(point => point.Y > 0.3 ? Brushes.Red : Brushes.LightGreen),
Values = new ChartValues<ObservablePoint>
{
new ObservablePoint(0, 5),
new ObservablePoint(20, 0),
new ObservablePoint(30, 5),
new ObservablePoint(40, 0),
new ObservablePoint(80, 5),
}
}
};
// Add two sections at x=20 and x=30
this.SectionsCollection = new SectionsCollection()
{
new AxisSection()
{
Value = 20,
Stroke = Brushes.Red,
StrokeThickness = 1
},
new AxisSection()
{
Value = 30,
SectionWidth = 50,
Stroke = Brushes.Red,
StrokeThickness = 1
}
};
}
public Func<double, string> LabelFormatter => value => $"{value}ms";
public SeriesCollection SeriesCollection { get; set; }
public SectionsCollection SectionsCollection { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Chart View
<Window>
<Window.DataContext>
<DataModel />
</Window.DataContext>
<CartesianChart Height="500"
Series="{Binding SeriesCollection}">
<CartesianChart.AxisX>
<Axis Sections="{Binding SectionsCollection}"
LabelFormatter="{Binding LabelFormatter}">
<Axis.Separator>
<Separator Step="10" />
</Axis.Separator>
</Axis>
</CartesianChart.AxisX>
<CartesianChart.AxisY>
<Axis>
<Axis.Separator>
<Separator Step="5" />
</Axis.Separator>
</Axis>
</CartesianChart.AxisY>
</CartesianChart>
</Window>

How to handle charts directly in code

I started working with C# and Live Charts a few days ago.
Based on examples found in the internet, I made a very simple graph in order to understand the concepts behind it.
That is what I have (it is working perfectly fine):
public partial class Wdw_graph : Window
{
public SeriesCollection SeriesCollection { get; set; }
public string[] Labels { get; set; }
public Wdw_graph(List<dated_value> serie)
{
InitializeComponent();
SeriesCollection = new SeriesCollection();
ColumnSeries col_serie = new ColumnSeries {
Values = new ChartValues<double>(),
DataLabels = true };
Labels = new string[serie.Count];
for(int i = 0; i < serie.Count; i++)
{
col_serie.Values.Add(serie[i].value);
Labels[i] = serie[i].date;
}
SeriesCollection.Add(col_serie);
DataContext = this;
}
}
<Grid>
<lvc:CartesianChart Series="{Binding SeriesCollection}" >
<lvc:CartesianChart.AxisX>
<lvc:Axis Labels="{Binding Labels}" LabelsRotation="80">
<lvc:Axis.Separator>
<lvc:Separator IsEnabled="False" Step="1"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
<lvc:CartesianChart.AxisY>
<lvc:Axis Title="Sold Apps"></lvc:Axis>
</lvc:CartesianChart.AxisY>
</lvc:CartesianChart>
</Grid>
As you can see, I have 'SeriesCollection' and 'Labels' handled in the class code and them they are bound to the XAML.
I would like to know if it is possible to use the same approach to handle other graph elements, such as the 'AxisX'. If so, how can I do it?
In this post "Change the format of the axis tick labels in LiveCharts" there is a code that shows what I would like to do:
cartesianChart2.AxisX.Add(new Axis
{
Name = "xAxis",
Title = "DateTime",
FontSize = 22,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = _amountValues,
});
But I could not reproduce that. I can't figure out where the 'cartesianChart2' comes from.
Well, I learned how to refer to the XAML elements in the class code.
I added 'x:Name="something"' to the CartesianChart and removed everything else (not using 'Binding' anymore):
<Grid>
<lvc:CartesianChart x:Name="cartesian_chart">
</lvc:CartesianChart>
</Grid>
Then, in the class code I inserted the axis and serie:
public partial class Wdw_graph : Window
{
public Wdw_graph(List<dated_value> serie)
{
InitializeComponent();
cartesian_chart.AxisX.Add(new Axis
{
Name = "xAxis",
Title = "Date and Time",
FontSize = 20,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = serie.Count,
Labels = new String[serie.Count],
});
cartesian_chart.AxisY.Add(new Axis
{
Name = "yAxis",
Title = "Currency",
FontSize = 20,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = 10,
});
cartesian_chart.AxisX[0].Separator.Step = 1;
cartesian_chart.AxisX[0].LabelsRotation = 80;
ColumnSeries col_serie = new ColumnSeries {
Values = new ChartValues<double>(),
DataLabels = true };
for(int i = 0; i < serie.Count; i++)
{
col_serie.Values.Add(serie[i].value);
cartesian_chart.AxisX[0].Labels[i] = serie[i].date;
}
cartesian_chart.Series.Add(col_serie);
DataContext = this;
}
}
I really preferred to handle the graph directly from the code, instead of using the XAML.
Can you tell me what are the drawbacks of this approach?

Get checkbox item from listbox in WPF

I am developing WPF application. In which I am adding CheckBoxes to a ListBox in following way.
foreach (User ls in lst)
{
AddContacts(ls, lstContactList);
}
private void AddContacts(User UserData, ListBox lstbox)
{
try
{
var txtMsgConversation = new CheckBox()
{
Padding = new Thickness(1),
IsEnabled = true,
//IsReadOnly = true,
Background = Brushes.Transparent,
Foreground = Brushes.White,
Width = 180,
Height = 30,
VerticalAlignment = VerticalAlignment.Top,
VerticalContentAlignment = VerticalAlignment.Top,
Content = UserData.Name, //+ "\n" + UserData.ContactNo,
Margin = new Thickness(10, 10, 10, 10)
};
var SpConversation = new StackPanel() { Orientation = Orientation.Horizontal };
SpConversation.Children.Add(txtMsgConversation);
var item = new ListBoxItem()
{
Content = SpConversation,
Uid = UserData.Id.ToString(CultureInfo.InvariantCulture),
Background = Brushes.Black,
Foreground = Brushes.White,
BorderThickness = new Thickness(1),
BorderBrush = Brushes.Gray
};
item.Tag = UserData;
lstbox.Items.Add(item);
}
catch (Exception ex)
{
//Need to log Exception
}
}
Now I need to get the checked items from ListBox. How do I proceed here, I tried below code, which returning null,
CheckBox chkBox = lstContactList.SelectedItem as CheckBox;
Thoughts?
The way of creating dynamic multiple items in a listbox is not in codebehind, but to create a template for the items, and then bind it to a list of items.
Example
Say I have a bunch of passages List<Passage> Passages { get; set; }:
public class Passage
{
public string Name { get; set; }
public bool IsSelected { get; set; }
}
In my xaml I create a template and bind to it
<ListBox ItemsSource="{Binding Passages}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" />
<TextBlock Text="{Binding Path=Name, StringFormat=Passage: {0}}"
Foreground="Blue" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The result looks like this for my four passages "Alpha", "Beta", "Gamma" and "I-25":
Then if I want the selected item, such as the recently checked Beta above, I just enumerate my List for the selected one(s).
var selecteds = Passages.Where(ps => ps.IsSelected == true);
Need to list different types objects in one ListBox? Say from binding to a composite collection or an ObservableCollection<T>?
See my answers here:
Composite Collection
ObservableCollection

How to Vertically "center" align the multi line text

Sorry if the title a bit misleading, I have difficulty to entitle it. It's also not exactly "center". (see below for further explanations)
Hi, I am going to make a project to write numbered musical scores. But I found an obstacle on leveling the "font". (See below picture for visual)
I need 1 2 3 3 1 2 3 4 4 to be in a straight "line"
I use WrapPanel as the container of my notes (as in the screen).
The main problem is on the pitch dot, which located EITHER above or below the note. Pitch is bound to the note, so I need process it in 1 User Control. But, if so, then I can't control the placement of the note "font" to be in one line.
Below is my note user control code :
XAML
<UserControl x:Class="RevisiConverter.NumericNoteBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" Width="{Binding ActualWidth, ElementName=txbNote}"
Height="{Binding ActualHeight, ElementName=mainStack}">
<StackPanel Name="mainStack">
<StackPanel Name="topStack">
</StackPanel>
<Canvas Name="canvas" Width="{Binding ActualWidth, ElementName=txbNote}" Height="{Binding ActualHeight, ElementName=txbNote}"
HorizontalAlignment="Center">
<TextBlock Name="txbNote" Text="1" Foreground="Black" FontSize="15"
Margin="0,0,0,-3" FontFamily="Courier" Canvas.Top="0" Canvas.Left="0"/>
</Canvas>
<StackPanel Name="botStack">
</StackPanel>
</StackPanel>
XAML.CS
public partial class NumericNoteBox : UserControl
{
private Note _child;
private Line _sharp;
public Note Child
{
get { return _child; }
set
{
_child = value;
Update();
}
}
public NumericNoteBox()
{
InitializeComponent();
_sharp = null;
}
public void Update()
{
txbNote.Text = _child.MainNote.ToString();
if (_sharp != null)
{
_sharp.Visibility = Visibility.Hidden;
}
topStack.Children.Clear();
botStack.Children.Clear();
if (_child != null)
{
if (_child.Pitch > 0)
{
for (int i = 0; i < _child.Pitch; i++)
{
topStack.Children.Add(new Ellipse());
(topStack.Children[topStack.Children.Count - 1] as Ellipse).Width = 3;
(topStack.Children[topStack.Children.Count - 1] as Ellipse).Height = 3;
(topStack.Children[topStack.Children.Count - 1] as Ellipse).Fill = Brushes.Black;
if (_child.Accidental != Note.Accidentals.Flat)
{
(topStack.Children[topStack.Children.Count - 1] as Ellipse).Margin = new Thickness(0, 1, 0, 0);
}
else
{
(topStack.Children[topStack.Children.Count - 1] as Ellipse).Margin = new Thickness(8, 1, 0, 0);
}
}
}
else if (_child.Pitch < 0)
{
for (int i = 0; i < Math.Abs(_child.Pitch); i++)
{
botStack.Children.Add(new Ellipse());
(botStack.Children[botStack.Children.Count - 1] as Ellipse).Width = 3;
(botStack.Children[botStack.Children.Count - 1] as Ellipse).Height = 3;
(botStack.Children[botStack.Children.Count - 1] as Ellipse).Fill = Brushes.Black;
if (_child.Accidental != Note.Accidentals.Flat)
{
(botStack.Children[botStack.Children.Count - 1] as Ellipse).Margin = new Thickness(0, 1, 0, 0);
}
else
{
(botStack.Children[botStack.Children.Count - 1] as Ellipse).Margin = new Thickness(8, 1, 0, 0);
}
}
}
if (_child.Accidental == Note.Accidentals.Flat)
{
txbNote.Text = "b" + _child.MainNote.ToString();
}
else if (_child.Accidental == Note.Accidentals.Sharp)
{
if (_sharp == null)
{
_sharp = new Line();
_sharp.X1 = 10;
_sharp.Y1 = 2.5;
_sharp.X2 = -2.5;
_sharp.Y2 = 12.5;
_sharp.StrokeThickness = 1;
_sharp.Stroke = Brushes.Black;
canvas.Children.Add(_sharp);
}
_sharp.Visibility = Visibility.Visible;
}
}
}
}
Note :
I'm not bound to that code, so if any of you have dealt to things like this more efficiently with totally different approach, then it's always welcome.
Sorry for some grammar error, if any, since english is not my first language.
I feel that I have not explained my problem real clear, since I also quite confused on it, so, please clarify anything you need.
Thanks
ADDITIONAL DETAILS :
The dot theoretically doesn't have certain limitation, but in practice, it's usually only 3 maximum (either 3 on top or 3 on bottom - not both)
In my code above, the note user control is divided into 3 grid row, the top one is for stacking the top dot, the mid one is for visualizing the note (numbers) and the bot one is for stacking the bot dot.
Please clarify anything, and I'll add more.
You should do this with an ItemsControl which use an appropriate ItemTemplate for the numeric notes.
First, create a ViewModel that defines a collection of "numeric note" items:
public class NumericNoteItem
{
public int Number { get; set; }
public int Pitch { get; set; }
}
public class ViewModel
{
public ViewModel()
{
NumericNotes = new ObservableCollection<NumericNoteItem>();
}
public ObservableCollection<NumericNoteItem> NumericNotes { get; private set; }
}
In the constructor of your MainWindow, you may set the DataContext to an instance of this ViewModel like this:
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
vm.NumericNotes.Add(new NumericNoteItem { Number = 1, Pitch = 0 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 2, Pitch = 1 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 3, Pitch = -1 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 4, Pitch = 0 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 5, Pitch = 2 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 6, Pitch = -2 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 4, Pitch = 0 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 5, Pitch = 3 });
vm.NumericNotes.Add(new NumericNoteItem { Number = 6, Pitch = -3 });
DataContext = vm;
}
For visualizing the pitch I'd recommend to use a Path object with a Geometry that consists of a number of EllipseGeometries. To achieve this you would implement a binding converter that converts the pitch number into a Geometry, like shown below. It uses the parameter argument of the Convert method to create a Geometry for either positive or negative pitch values.
public class NotePitchConverter : IValueConverter
{
private const double radius = 1.5;
private const double distance = 2 * radius + 1;
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var pitch = (int)value;
var geometry = new GeometryGroup();
if (parameter as string == "Bottom")
{
pitch = -pitch;
}
for (int i = 0; i < pitch; i++)
{
geometry.Children.Add(new EllipseGeometry(
new Point(radius, radius + i * distance), radius, radius));
}
return geometry;
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Now, in XAML you would create an instance of this converter in e.g. the MainWindow Resources:
<Window.Resources>
<local:NotePitchConverter x:Key="NotePitchConverter"/>
</Window.Resources>
and write the ItemsControl like shown below. Note that the DataTemplate uses a fixed height for the Path elements that show the top and bottom pitch. If you need to show more than three dots, you'd need to increase their height.
<ItemsControl ItemsSource="{Binding NumericNotes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="12"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="12"/>
</Grid.RowDefinitions>
<Path Grid.Row="0" Fill="Black" HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Data="{Binding Pitch,
Converter={StaticResource NotePitchConverter}}"/>
<TextBlock Grid.Row="1" Text="{Binding Number}"/>
<Path Grid.Row="2" Fill="Black" HorizontalAlignment="Center"
VerticalAlignment="Top"
Data="{Binding Pitch,
Converter={StaticResource NotePitchConverter},
ConverterParameter=Bottom}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

How do you get Canvas.SetTop to work on ItemsCollection items?

Below is the complete program (XAML and code behind). When started you will see:
When you click [Show/Hide] you see:
When you uncheck Home and Documents and click [Apply] you see:
As you can see, the Home and Documents folder disappear, but the other folders do not move up. Looking at the two list boxes you see three items per line, folder label, the value for Canvas.SetTop, and True: Visibility.Visible or False: Visibility.Collapsed. The Original Placement list box are the original values. Each time you click [Apply] the New Placement List box shows that new values. The Visibility value gets applied and the result is visible, either the folder is there or it is not. However, applying the Canvas.SetTop does not work. And you can see that the values have changed for the Canvas.SetTop. And, if you look at the code, you see that it is applied to the same UIElement as the Visibility is applied to:
var f = Me[i];
Canvas.SetTop(iec, f.Top); // Why does this not work and
iec.Visibility = f.FolderVisible; // this does work?
I need the folders that are visible to move up to take the place of those that are collapsed. Canvas.SetTop is not working. How can I get the Folders to move?
Here is the Solution Explorer pane so that you know what is needed:
Here is the XAML:
<Window x:Class="FolderTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:FolderTest"
Title="Folder Test"
Width="450"
Height="410">
<Grid Margin="10">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<!-- Folder Show/Hide Button -->
<Button Name="FolderOptions" Content="Show/Hide" FontSize="8" FontWeight="Bold" Width="50" Height="20" Margin="116, 0, 0, 0" Click="Event_ShowHide_ButtonClick" />
<!-- Folders -->
<Grid>
<Canvas Name="xFolders"
Width="170"
Height="309"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="0,10,0,0">
<Canvas.Resources>
<local:Folders x:Key="myFolders" />
</Canvas.Resources>
<ItemsControl ItemsSource="{Binding Source={StaticResource myFolders}}">
<ItemsControl.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
</DataTemplate.Resources>
<Canvas Name="FolderCanvas"
Tag="{Binding Path=Number}"
Visibility="{Binding Path=FolderVisible}">
<Path Data="{Binding Path=FolderPath}"
Stroke="{Binding Path=Brush}"
Fill="{Binding Path=Brush}"/>
<StackPanel Orientation="Horizontal"
Canvas.Left="5"
Canvas.Top="2">
<TextBlock Text="{Binding Path=Label}"
FontSize="12"
Margin="20, 0, 0, 0" />
</StackPanel>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top"
Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Canvas>
</Grid>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10, 0, 0, 0">
<TextBlock Text="Original Placemet:" />
<ListBox Name="OriginalPlacement" />
<Button Name="Refresh" Content="New Placemet:" Margin="0, 10, 0, 0" Click="Event_OriginalPlacement_ButtonClick" />
<ListBox Name="NewPlacement" />
</StackPanel>
</StackPanel>
</Grid>
</Window>
Here is the C# Code Behind:
using System;
using System.Collections.Generic;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows;
using System.Collections.ObjectModel;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace FolderTest
{
public struct GD
{
public static MainWindow MainWindow = null;
public static Button Refresh = null;
}
public partial class MainWindow : Window
{
public MainWindow()
{
GD.MainWindow = this;
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
GD.Refresh = Refresh;
Folders.Placement(ref OriginalPlacement);
}
private void Event_ShowHide_ButtonClick(object sender, RoutedEventArgs e)
{
var pu = Folders.FolderShowPopUp();
pu.PlacementTarget = FolderOptions;
pu.IsOpen = true;
}
private void Event_OriginalPlacement_ButtonClick(object sender, RoutedEventArgs e)
{
Folders.Placement(ref NewPlacement);
}
}
public class Folder
{// Specification for each folder tab seen on the left.
public int Number { get; set; } // Folder sequence number top to bottom starging with 1.
public string Label { get; set; } // The label that appears on the folder tab itself.
public double Top { get; set; } // Position of the upper left corner of this folder.
public SolidColorBrush Brush { get; set; } // Solid Color Brush.
public string FolderPath { get; set; } // Contains the geometric path to draw this folder and its tab.
public Visibility FolderVisible { get; set; } // This folder's Show/Hide flag.
public Folder(int Number, string Label, double Top, SolidColorBrush Brush, string FolderPath, Visibility FolderVisible)
{
this.Number = Number;
this.Label = Label;
this.Top = Top;
this.Brush = Brush;
this.FolderPath = FolderPath;
this.FolderVisible = FolderVisible;
}
}
public class Folders : ObservableCollection<Folder>
{
public static ObservableCollection<Folder> Me = null;
private static string[] Labels = new string[]
{
"Personal",
"Health",
"Finances",
"Home",
"Employment",
"Insurance",
"Documents",
"Contacts",
"Journal"
};
private static Dictionary<string, Tuple<bool, bool>> LabelData = new Dictionary<string, Tuple<bool, bool>>()
{// Label Show Hidable
{Labels[0], new Tuple<bool, bool>(true, false)},
{Labels[1], new Tuple<bool, bool>(true, true)},
{Labels[2], new Tuple<bool, bool>(true, true)},
{Labels[3], new Tuple<bool, bool>(true, true)},
{Labels[4], new Tuple<bool, bool>(true, true)},
{Labels[5], new Tuple<bool, bool>(true, true)},
{Labels[6], new Tuple<bool, bool>(true, true)},
{Labels[7], new Tuple<bool, bool>(true, false)},
{Labels[8], new Tuple<bool, bool>(true, true)}
};
private static string[] FolderColors = new string[]
{
"FF36579E",
"FFDF2024",
"FF16A146",
"FF00B2D4",
"FFF47B20",
"FF9F1F63",
"FF13A89E",
"FFB7B7E7",
"FF50CAF5",
"FFAA9E74",
"FF86787D",
"FF36D146",
};
private static byte[] ARGBColor = new byte[4]; // Byte array for the folder top color.
private static void ColorString2ARGB(ref byte[] bytes, string Hex)
{// Converts 8 char hex string to 4 byte array.
for (int i = 0; i < 8; i += 2)
bytes[i / 2] = Convert.ToByte(Hex.Substring(i, 2), 16);
}
private static int colorIndex = -1; // Initial value of the colorIndex.
private static string NextColor()
{// Returns a 8 char string containing the next top FolderColor. If at end, cycle to beginning.
colorIndex++;
if (colorIndex >= FolderColors.Length) colorIndex = 0;
return FolderColors[colorIndex];
}
private class FolderShow
{
public string Label { get; set; }
public bool Show { get; set; }
public bool Hidable { get; set; }
public FolderShow(string label, bool show, bool hidable)
{
Label = label;
Show = show;
Hidable = hidable;
}
}
private static List<FolderShow> FolderShowList = null;
public static Popup FolderShowPopUp()
{
var pu = new Popup()
{
Placement = PlacementMode.Right,
AllowsTransparency = true,
StaysOpen = false
};
var b = new Border()
{
BorderThickness = new Thickness(2),
BorderBrush = new SolidColorBrush() { Color = Colors.Black },
CornerRadius = new CornerRadius(5),
Background = new SolidColorBrush() { Color = Colors.White }
};
var sp = new StackPanel()
{
Orientation = Orientation.Vertical,
Margin = new Thickness(10)
};
var tb = new TextBlock()
{
Text = "Checked Folders will be Displayed: ",
FontSize = 16,
FontWeight = FontWeights.Bold,
};
sp.Children.Add(tb);
foreach (var fs in FolderShowList)
{
var cb = new CheckBox()
{
Content = fs.Label,
IsChecked = fs.Show,
IsEnabled = fs.Hidable,
FontSize = 14,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 5, 0, 0)
};
cb.Click += new RoutedEventHandler(Event_CheckBoxFolderList_Click);
sp.Children.Add(cb);
}
var bp = new StackPanel()
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 5, 0, 0)
};
var ba = new Button() { Content = "Apply", Width = 50, Height = 25, BorderBrush = new SolidColorBrush(Colors.Transparent), Tag = pu };
ba.Click += new RoutedEventHandler(Event_ApplyFolderList_Click);
bp.Children.Add(ba);
var bc = new Button() { Content = "Cancel", Width = 50, Height = 25, BorderBrush = new SolidColorBrush(Colors.Transparent), Tag = pu, Margin = new Thickness(7, 0, 0, 0) };
bc.Click += new RoutedEventHandler(Event_CancelFolderList_Click);
bp.Children.Add(bc);
sp.Children.Add(bp);
var tbm = new TextBlock()
{
Text = "Disabled folders cannot be hidden.",
Margin = new Thickness(0, 5, 0, 0),
FontSize = 12,
Foreground = new SolidColorBrush() { Color = Colors.Red }
};
sp.Children.Add(tbm);
b.Child = sp;
pu.Child = b;
return pu;
}
private static void Event_CheckBoxFolderList_Click(object sender, RoutedEventArgs e)
{
var cb = (CheckBox)e.Source;
foreach (var fs in FolderShowList)
{
if (fs.Label == cb.Content as string)
{
fs.Show = (bool)cb.IsChecked;
break;
}
}
}
private static void Event_CancelFolderList_Click(object sender, RoutedEventArgs e)
{
((Popup)((Button)e.Source).Tag).IsOpen = false;
}
private static void Event_ApplyFolderList_Click(object sender, RoutedEventArgs e)
{
foreach (var fs in FolderShowList)
{
var sh = LabelData[fs.Label];
LabelData[fs.Label] = new Tuple<bool, bool>(fs.Show, sh.Item2);
}
((Popup)((Button)e.Source).Tag).IsOpen = false;
int p = 0;
foreach (var f in Me)
{
var fs = LabelData[f.Label].Item1 == true;
f.Top = p * folderPositionFactor;
f.FolderVisible = fs ? Visibility.Visible : Visibility.Collapsed;
if (fs) p += 1;
}
foreach (ItemsControl ic in GD.MainWindow.xFolders.Children)
{
for (int i = 0; i < ic.Items.Count; i++)
{
var ie = (UIElement)ic.ItemContainerGenerator.ContainerFromIndex(i);
var iec = Controls.FindChildByType<Canvas>(ie, "FolderCanvas");
if (iec != null)
{
var f = Me[i];
Canvas.SetTop(iec, f.Top); // Why does this not work and
iec.Visibility = f.FolderVisible; // this does work?
}
}
break;
}
GD.Refresh.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
public static void Placement(ref ListBox lb)
{
lb.Items.Clear();
foreach (ItemsControl ic in GD.MainWindow.xFolders.Children)
{
for (int i = 0; i < ic.Items.Count; i++)
{
var ie = (UIElement)ic.ItemContainerGenerator.ContainerFromIndex(i);
var iec = Controls.FindChildByType<Canvas>(ie, "FolderCanvas");
if (iec != null)
{
var f = Me[i];
lb.Items.Add(f.Label + ": " + f.Top.ToString() + ", " + (f.FolderVisible == Visibility.Visible ? "True" : "False"));
}
}
break;
}
}
public Folders()
{// The constructor, initializes itself by creating a folder for each label in Labels
FolderShowList = new List<FolderShow>();
for (int i = 0; i < Labels.Length; i++)
{
string n = Labels[i];
bool h = LabelData[Labels[i]].Item2;
bool s = h == true ? LabelData[Labels[1]].Item1 == true : true;
FolderShowList.Add(new FolderShow(n, s, h));
}
SetFolders();
Me = this;
}
const double folderPositionFactor = 31; // 21.80;
public void SetFolders()
{
SolidColorBrush[] scb = new SolidColorBrush[Labels.Length]; // Hold the linear solid color brush to assign to the folder.
Color[] pointerColor = new Color[Labels.Length]; // Hold the color to assign to the folder's solid color brush.
for (int i = 0; i < Labels.Length; i++)
{// Create a solid color brush for each folder.
ColorString2ARGB(ref ARGBColor, NextColor()); // Color into byte array.
Color TmpColor = Color.FromArgb(ARGBColor[0], ARGBColor[1], ARGBColor[2], ARGBColor[3]); // Create top color.
pointerColor[i] = TmpColor;
SolidColorBrush TmpSCB = new SolidColorBrush() { Color = TmpColor };
scb[i] = TmpSCB; // Assign the solid color brush.
}
// All is ready to create the individual folders.
const string folderHeight = "56"; // "44";
Brush FontColor = Brushes.Black; // Initial font color for labels.
string fp = "M0,7 A7,7 90 0 1 7,0 L100,0 105,18 150,18 150,FH 0,FH Z".Replace("FH", folderHeight); // Initial geometric path for folder design.
int afp = 0; // Actual Folder Position.
for (int i = 0; i < Labels.Length; i++)
{// Create the individual folders.
bool fs = FolderShowList[i].Show;
Add(new Folder(
i + 1, // Folder sequence count.
Labels[i], // Folder label.
afp * folderPositionFactor, // Position of top of folder.
scb[i % scb.Length], // Solid color brush.
fp, // Geometric path for folder design.
fs ? Visibility.Visible : Visibility.Collapsed // User Hidden.
)
);
if (fs) afp += 1;
if (i == 0)
{// First folder created, now set values for remaining folders.
FontColor = Brushes.White;
fp = "M0,25 A7,7 90 0 1 7,18 L13,18 18,0 100,0 105,18 150,18 150,FH 0,FH Z".Replace("FH", folderHeight);
}
}
}
}
public static class Controls
{
public static T FindChildByType<T>(DependencyObject parent, string childName) where T : DependencyObject
{
if (parent == null) return null; // No parent, get out of here.
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChildByType<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
}
}
Thanks for any help that you can provide.
I finally figured out the answer, and it is very simple.
This is the code to be changed (the center line only):
var f = Me[i];
Canvas.SetTop(iec, f.Top); // Why does this not work
iec.Visibility = f.FolderVisible; // and this does work?
This is the new code (only the center line is changed, actually, only one character is removed):
var f = Me[i];
Canvas.SetTop(ie, f.Top);
iec.Visibility = f.FolderVisible;
That is, change Canvas.SetTop(iec, f.Top); to Canvas.SetTop(ie, f.Top); that is, only change iec to ie and all works fine.

Categories

Resources