I have two ListBoxs defined in my XAML and one Class MyListItem.
Now one ListBox should display the name as button and the second ListBox should display the name as a TextBlock.
Here a little example, both ListBoxs behave the same.
MyListItem
public class MyListItem
{
private string _name;
public string Name
{
get{return _name;}
set{_name = value;}
}
}
XAML
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DataTemplate.Views.MainWindow"
xmlns:viewsmodels="clr-namespace:DataTemplate.ViewModels;assembly=DataTemplate"
xmlns:dt="clr-namespace:DataTemplate;assembly=DataTemplate"
Title="DataTemplate" Width="700">
<Window.DataContext>
<viewsmodels:MainWindowViewModel />
</Window.DataContext>
<Grid ColumnDefinitions="250,250,250">
<ItemsControl Grid.Column="1" Items="{Binding List2}">
<ItemsControl.DataTemplates>
<DataTemplate DataType="{x:Type dt:MyListItem}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<ItemsControl Grid.Column="2" Items="{Binding List3}">
<ItemsControl.DataTemplates>
<DataTemplate DataType="{x:Type dt:MyListItem}">
<Button Content="{Binding Name}"/>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Grid>
</Window>
ViewMode
public class MainWindowViewModel
{
public ObservableCollection<MyListItem> List1 { get; set; }
public ObservableCollection<MyListItem> List2 { get; set; }
public ObservableCollection<MyListItem> List3 { get; set; }
public MainWindowViewModel()
{
List1 = new ObservableCollection<MyListItem>();
List2 = new ObservableCollection<MyListItem>();
List3 = new ObservableCollection<MyListItem>();
Random rand = new Random();
for (int i = 0; i < rand.Next(1, 20); i++)
{
MyListItem mli = new MyListItem();
mli.Name = "ListItem" + i;
List1.Add(mli);
}
for (int i = 0; i < rand.Next(1, 20); i++)
{
MyListItem mli = new MyListItem();
mli.Name = "ListItem" + i;
List2.Add(mli);
}
for (int i = 0; i < rand.Next(1, 20); i++)
{
MyListItem mli = new MyListItem();
mli.Name = "ListItem" + i;
List3.Add(mli);
}
}
}
Unfortunately there's currently no good way to do this in Avalonia that I can think of. The most obvious way would be to add the data templates to a <Style.Resources> collection and use {StyleResource} to reference them, but this doesn't work currently.
I think you have two alternatives here for the moment:
Just copy and paste the data templates into the ItemsControl.ItemTemplate
Define the data templates in code and reference them using {Static}. For this you can use FuncDataTemplate<>
I've added an issue to track this problem here: https://github.com/AvaloniaUI/Avalonia/issues/1020
You need to use ItemsControl instead of ListBox and have ItemTemplate set differently for each of them.
One will point to DataTemplate(using x:Key, not DataType) with TextBlock, and the other to DataTemplate with Button.
Related
I'm currently using the MVVM pattern in one of my apps, to be more specific I'm using the MVVMLight framework. In one of the pages, I will have a screen where the user can input the width and length to draw rectangles, there is not much code logic so, I was thinking to put all of my code in the code-behind since most of what will be happening in this screen is UI related.
Does that make sense to use the code-behind in this case? If not, how would you structure the code to use the MVVM pattern, what would you put in the ViewModel in this case and what would you put in your code behind?
Here is the code without using MVVM.
XAML:
<Window x:Class="DrawingRectangles.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:DrawingRectangles"
mc:Ignorable="d"
Title="MainWindow" Height="531.798" Width="782.115">
<Grid Name="MyGrid" Width="480" Height="240" Margin="27,23,267,174">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="59*"/>
<ColumnDefinition Width="421*"/>
</Grid.ColumnDefinitions>
<Canvas Name="MyCanvas" Background="#FFF1F0F0" Margin="10" Grid.ColumnSpan="2"/>
<Grid Margin="10,235,10,-92" Background="WhiteSmoke" Grid.ColumnSpan="2">
<Button x:Name="drawButton" Content="Draw" Click="drawButton_Click"/>
<Button x:Name="resetButton" Content="Reset" Click="resetButton_Click"/>
<TextBox x:Name="textBoxPartWidth"/>
<TextBox x:Name="textBoxPartLength"/>
</Grid>
</Grid>
</Window>
CODE-BEHIND:
namespace DrawingRectangles
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void drawButton_Click(object sender, RoutedEventArgs e)
{
clearScreen();
int xParts = 10;
int yParts = 10;
for (int i = 0; i < xParts; i++) {
for (int j = 0; j < yParts; j++) {
// Create a rectangle.
Rectangle myRectangle = new Rectangle();
myRectangle.Width = Convert.ToDouble(textBoxPartLength.Text);
myRectangle.Height = Convert.ToDouble(textBoxPartWidth.Text);
myRectangle.Margin = new Thickness((Convert.ToInt32(myRectangle.Width) + 1) * i, (Convert.ToInt32(myRectangle.Height) + 1) * j, 0, 0);
myRectangle.Fill = new SolidColorBrush(Color.FromArgb(170, 51, 51, 255));
MyCanvas.Children.Add(myRectangle);
}
}
}
private void resetButton_Click(object sender, RoutedEventArgs e)
{
MyCanvas.Children.Clear();
}
private void clearScreen()
{
MyCanvas.Children.Clear();
}
}
}
UI
EDIT:
Second Image (reference only):
The Button in the view should be bound to an ICommand property of the view model. The command will be executed when you click on the Button. Please refer to this blog post for information about how to handle events in an MVVM application. In MvvmLight, the ICommand implementation is called RelayCommand.
You should also bind the Text properties of the TextBoxes to two source properties of the view model and the Canvas element in your view should be replaced with an ItemsControl that you bind to a collection of objects that are defined in the view model.
Please refer to the following sample code.
Model:
public class Model
{
public int Width { get; set; }
public int Height { get; set; }
public Thickness Margin { get; set; }
public Brush Fill { get; set; }
}
View:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="#FFF1F0F0" Margin="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="{Binding Width}"
Height="{Binding Height}"
Margin="{Binding Margin}"
Fill="{Binding Fill}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Draw" Command="{Binding DrawCommand}" />
<Button Content="Reset" Command="{Binding ResetCommand}" />
<TextBox Text="{Binding Width}"/>
<TextBox Text="{Binding Height}"/>
View Model:
public class ViewModel
{
public ViewModel()
{
DrawCommand = new RelayCommand(Draw);
ResetCommand = new RelayCommand(Clear);
}
public ObservableCollection<Model> Items { get; } = new ObservableCollection<Model>();
public RelayCommand DrawCommand { get; }
public RelayCommand ResetCommand { get; }
public int Width { get; set; }
public int Height { get; set; }
private void Draw()
{
Clear();
int xParts = 10;
int yParts = 10;
for (int i = 0; i < xParts; i++)
{
for (int j = 0; j < yParts; j++)
{
Model model = new Model();
model.Width = Width;
model.Height = Height;
model.Margin = new Thickness((model.Width + 1) * i, (model.Height + 1) * j, 0, 0);
model.Fill = new SolidColorBrush(Color.FromArgb(170, 51, 51, 255));
Items.Add(model);
}
}
}
private void Clear()
{
Items.Clear();
}
}
In this example all application logic has been moved to the view model where it belongs. There is no logic left in the code-behind class of the view.
Also note that the view models creates instances of Model objects rather than creating Rectangle elements. It's generally considered be a bad practice to reference UI elements in a view model class. The Rectangle elements are created by the ItemsControl. See the ItemTemplate in the view.
I managed to access control in the datatemplate of a GridViewItem, the following code:
private void btnChangePhoneNumber_Click(object sender, RoutedEventArgs e)
{
GridCell.SelectedItem = GridCell.Items[3];
var container = GridCell.ContainerFromIndex(3);
var _children = AllChildren(container);
var _control = _children.First(c => c.Name == "PhoneNumber");
_control.text = "123456789";
}
public List<TextBlock> AllChildrenText(DependencyObject parent)
{
var _List = new List<TextBlock> { };
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var _Child = VisualTreeHelper.GetChild(parent, i);
if (_Child is TextBlock)
{
_List.Add(_Child as TextBlock);
}
_List.AddRange(AllChildrenText(_Child));
}
return _List;
}
where the GridCell is a Gridview.
This work.. but..
If I implement GridView with less than 40 items it's all right.
Unlike if I implement gridView with 10000 items, the text change that happens with the method: btnChangePhoneNumber_Click, also happens in other items ... and I can not understand the reason since, in the btnChangePhoneNumber_Click method, only one item is chosen.
Thanks in advance. A greeting.
I have tested your code, but I could not reproduce your issue in my side. As far as I'm concerned, It is low performance to render 10000 items in your GridView. And using VisualTreeHelper will bring about worse performance. You could bind
the text of TextBlock in the datatemplate with mvvm ViewModel. You just need
to modify the view model and the text of TextBlock will be changed. For more please refer to Data binding in depth. And the following is segment code of ViewModel.
MainPageViewModel.cs
public class MainPageViewModel : ViewModelBase
{
private ObservableCollection<Phone> _items;
public ObservableCollection<Phone> Items
{
get
{
return _items;
}
set
{
_items = value;
OnPropertyChanged();
}
}
public MainPageViewModel()
{
var list = new ObservableCollection<Phone>();
for (var i = 0; i < 1000; i++)
{
list.Add(new Phone { PhoneNumber = "123456" });
}
_items = list;
}
}
MainPage.xaml
<Page.DataContext>
<local:MainPageViewModel x:Name="ViewModel"/>
</Page.DataContext>
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Click="btnChangePhoneNumber_Click" Content=" click me"/>
<GridView x:Name="GridCell" Height="400" ItemsSource="{Binding Items}" >
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:Phone">
<TextBlock Text="{x:Bind PhoneNumber ,Mode=OneWay}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
I have upload the code sample to github. Please check!
I'm using the following code for binding
XAML
<StackPanel x:Name="channelsRecordTimeData" Orientation="Vertical">
<ItemsControl x:Name="channelRecordTimeItems" ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="gridChannelRecordTimeItem" Width="{Binding Path=ChannelRecordTimeItemWidth}"
Height="{Binding Path=ChannelRecordTimeItemHeight}" Margin="{Binding Path=ChannelRecordTimeItemsMargin}"
HorizontalAlignment="Left" DataContext="{Binding Path=ListRecordTime}">
<Grid.Background>
<ImageBrush x:Name="gridChannelRecordTimeItemBgr" ImageSource="..\Resources\playback_grid_channel_record_time_item_bgr_normal.png"/>
</Grid.Background>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
C#
public class DATA
{
public double ChannelRecordTimeItemWidth { set; get; }
public double ChannelRecordTimeItemHeight { set; get; }
public Thickness ChannelRecordTimeItemsMargin { set; get; }
public List<RecordTime> ListRecordTime { set; get; }
public DATA()
{
ChannelRecordTimeItemWidth = 1000;
ChannelRecordTimeItemHeight = 20;
ChannelRecordTimeItemsMargin = new System.Windows.Thickness(0, 0, 0, 0);
ListRecordTime = null;
}
}
public static List<DATA> listDATA = new List<DATA>();
for(int i = 0 ; i < 10 ; i++)
{
DATA data = new DATA();
listDATA.Add(data);
}
channelRecordTimeItems.ItemsSource = listDATA;
channelRecordTimeItems.Items.Refresh();
This code will notify to the XAML update when I use the line of code as
listDATA[0].ChannelRecordTimeItemWidth -= 15;
There is any way to XAML update properties automatically, when we manipulate on the listDATA as
listDATA.RemoveAt();
listDATA.Add();
listDATA.Clear();
Without calling the two following lines code
channelRecordTimeItems.ItemsSource = listDATA;
channelRecordTimeItems.Items.Refresh();
GUI will be updated only in case underlying source collection is implementing INotifyCollectionChanged which raise CollectionChanged events to refresh GUI components.
You can use ObservableCollection which internally provides you this feature.
Replace
public static List<DATA> listDATA = new List<DATA>();
with
public static ObservableCollection<DATA> listDATA = new ObservableCollection<DATA>();
I am trying to implement a list that contains items of a certain type, a Session. Each Session contains a list that contains the type Note. I want to display these Notes in the list under their respective Session header.
Currently I have tried two different methods. The first way was to use ItemsControls as ControlTemplate for the ListBoxItems. This is what I used in the picture below and it is how I want the list to look like. Each red rectangle shows a Session, the items below the header are the Notes. The problem then is that the selection from the ListBox selects ItemsControls instead of each separate Note.
The other way I tried to implement the list is to give each Note a property of which Session it belongs to in order to use a GroupStyle on the ListBox. If I then set the ItemsSource of the ListBox to a list of Notes instead of Sessions I'll get a list that looks like the picture and that has selection of notes. The problem now is that I want the list to show Sessions that doesn't contain any Notes as well.
Does anyone know what I should use to implement a list with selection and that works the way I have described?
MainWindow.xaml:
<TreeView ItemsSource="{Binding}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Session}" ItemsSource="{Binding Path=Notes}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:Note}">
<Expander Header="{Binding Path=Notek}">
<TextBlock Foreground="Red" Text="{Binding Path=Details}" />
</Expander>
</DataTemplate>
</TreeView.Resources>
</TreeView>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<Session> sessions = new List<Session>();
for (int i = 0; i < 5; i++)
{
List<Note> notes = new List<Note>();
for (int j = i * 5; j < (i + 1) * 5; j++)
{
Note note = new Note()
{
Notek = string.Format("Note {0}", j),
Details = string.Format("Note j = {0}{1}j*j = {2}", j, System.Environment.NewLine, j*j)
};
notes.Add(note);
}
Session session = new Session()
{
Name = string.Format("Session # {0}", i),
Notes = notes
};
sessions.Add(session);
}
DataContext = sessions;
}
}
public class Session
{
public string Name { get; set; }
public List<Note> Notes { get; set; }
}
public class Note
{
public string Notek { get; set; }
public string Details { get; set; }
}
I think that you can style your HierarchicalDataTemplate as you want. I just show you the example. I think its easier rather than ItemsControl with event handlers.
To create the answer I will assume the following data model:
class Session
{
public IEnumerable<Note> Notes { get; }
}
class Note { }
This requires some coding to sync up the list boxes. I have created an attached property called 'ListBoxGroup'. All listboxes with the same group name can only have a single shared selected item. It is quite a lot of code so it's at the bottom.
Important to note: The listboxgroup for a listbox cannot be changed after originally set, and it doesn't support removal of items, doesn't check for nulls etc. So if you need to change sessions at runtime you should remove items from their groups, check if a listbox is removed from the visual tree, etc.
First the XAML for the page:
xmlns:local="clr-namespace:YourApplication.YourNamespace"
<!-- ItemsControl does not have selection -->
<ItemsControl ItemsSource="{Binding SessionList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- Header for the session -->
<Border Background="Gray">
<TextBlock Text="{Binding Name}" />
</Border>
<!-- listbox for notes -->
<ListBox ItemsSource="{Binding Notes}" local:ListBoxGroup.GroupName="Group1">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- Template for a single note -->
<TextBlock Text="{Binding Description}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Below is C# code for the ListBoxGroup property:
public static class ListBoxGroup
{
public static string GetGroupName(DependencyObject obj)
{
return (string)obj.GetValue(GroupNameProperty);
}
public static void SetGroupName(DependencyObject obj, string value)
{
obj.SetValue(GroupNameProperty, value);
}
// Using a DependencyProperty as the backing store for GroupName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.RegisterAttached("GroupName", typeof(string), typeof(ListBoxGroup), new UIPropertyMetadata(null, ListBoxGroupChanged));
private static Dictionary<string, List<ListBox>> _listBoxes = new Dictionary<string, List<ListBox>>();
private static void ListBoxGroupChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
string newValue = e.NewValue as string;
ListBox listBox = obj as ListBox;
if (newValue == null || listBox == null) return;
if (_listBoxes.ContainsKey(newValue))
{
_listBoxes[newValue].Add(listBox);
}
else
{
_listBoxes.Add(newValue, new List<ListBox>() { listBox });
}
listBox.SelectionChanged += new SelectionChangedEventHandler(listBox_SelectionChanged);
listBox.PreviewKeyUp += new System.Windows.Input.KeyEventHandler(listBox_KeyUp);
}
static void listBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
ListBox listBox = sender as ListBox;
if (e.Key == System.Windows.Input.Key.Up && listBox.SelectedIndex == 0)
{
//move to previous
string groupName = GetGroupName(listBox);
List<ListBox> group = _listBoxes[groupName];
int senderIndex = group.IndexOf(listBox);
if (senderIndex != 0)
{
listBox.SelectedItem = null;
ListBox beforeSender = group[senderIndex - 1];
int index = beforeSender.Items.Count - 1;
beforeSender.SelectedIndex = index;
var container = beforeSender.ItemContainerGenerator.ContainerFromIndex(index);
(container as FrameworkElement).Focus();
}
}
else if (e.Key == System.Windows.Input.Key.Down
&& listBox.SelectedIndex == listBox.Items.Count - 1)
{
//move to next
string groupName = GetGroupName(listBox);
List<ListBox> group = _listBoxes[groupName];
int senderIndex = group.IndexOf(listBox);
if (senderIndex != group.Count - 1)
{
listBox.SelectedItem = null;
ListBox afterSender = group[senderIndex + 1];
afterSender.SelectedIndex = 0;
var container = afterSender.ItemContainerGenerator.ContainerFromIndex(0);
(container as FrameworkElement).Focus();
}
}
}
static void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
ListBox listBox = sender as ListBox;
string groupName = GetGroupName(listBox);
foreach (var item in _listBoxes[groupName])
{
if (item != listBox)
{
item.SelectedItem = null;
}
}
}
}
}
I'm new to WPF, and am having trouble getting the values for properties for a custom user control from the MainWindow XAML file.
Here, I want to get the value "8" as the number of rows and columns but in my InitializeGrid() method, the properties are never set. They are always "0". What am I doing wrong?
Any references will also be appreciated.
This is my MainWindow.xaml (the relevant portions):
<local:BoardView
BoardRows="8"
BoardColumns="8"
/>
This is my BoardView.xaml:
<UniformGrid
Name="uniformGrid"
Rows="{Binding BoardRows}"
Columns="{Binding BoardColumns}"
>
</UniformGrid>
</UserControl>
And this is my BoardView.xaml.cs:
[Description("The number of rows for the board."),
Category("Common Properties")]
public int BoardRows
{
get { return (int)base.GetValue(BoardRowsProperty); }
set { base.SetValue(BoardRowsProperty, value); }
}
public static readonly DependencyProperty BoardRowsProperty =
DependencyProperty.Register("BoardRows", typeof(int), typeof(UniformGrid));
[Description("The number of columns for the board."),
Category("Common Properties")]
public int BoardColumns
{
get { return (int)base.GetValue(BoardColumnsProperty); }
set { base.SetValue(BoardColumnsProperty, value); }
}
public static readonly DependencyProperty BoardColumnsProperty =
DependencyProperty.Register("BoardColumns", typeof(int), typeof(UniformGrid));
public BoardView()
{
InitializeComponent();
DataContext = this;
InitializeGrid();
}
private void InitializeGrid()
{
int rows = BoardRows;
int cols = BoardColumns;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
uniformGrid.Children.Add( ... );
// ...
}
}
}
You have this binding set up:
<UserControl ...>
<UniformGrid
Name="uniformGrid"
Rows="{Binding BoardRows}"
Columns="{Binding BoardColumns}"
>
</UniformGrid>
</UserControl>
The problem is that your binding is not working because the binding uses the default data source which is the DataContext of the UserControl. You probably haven't set the DataContext but that's OK because that's not what you want anyway.
You want to bind the number of Rows in the UniformGrid to the BoardView.BoardRows property. Since the UserControl is the previous code snippet is a BoardView, you can give the BoardView a name and use the ElementName syntax to refer to it like this:
<UserControl Name="boardView" ...>
<UniformGrid
Name="uniformGrid"
Rows="{Binding BoardRows, ElementName=boardView}"
Columns="{Binding BoardColumns, ElementName=boardView}"
>
</UniformGrid>
</UserControl>
This says: "Bind UniformGrid.Row to the BoardRows property of the element named boardView", just what you want!