Custom control and element binding - c#

I'm creating custom control which will contain grid with dynamically added (or removed) rows and columns. Each cell has to contain checkbox which will be binded with some model. If user clicks on checkbox, the dictionary should refresh.
Model.cs
public class Model
{
public Model(List<PanelModel> panels)
{
this.Panels = new Dictionary<int, bool>();
panels.ForEach(panel => this.Panels.Add(panel.Id, false));
}
...
public Dictionary<int, bool> Panels { get; set; }
}
CustomControl.cs
CheckBox checkBox = new CheckBox
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetRow(checkBox, index);
Grid.SetColumn(checkBox, innerIndex);
Binding binding = new Binding(string.Format("Panels[{0}]", panel.Id));
binding.Source = this.Configurations;
binding.Mode = BindingMode.TwoWay;
checkBox.SetBinding(CheckBox.IsCheckedProperty, binding);
this.mainGrid.Children.Add(checkBox);
How can I bind specific checkbox with element in dictionary Panels? Dictionary contains int - bool pair which means, that panel with ID x is on or off.
Now, if I click on checkbox, nothing happenes. Debugger shows, that all panels are false (off).

From what you describe, I would approach this with an ItemsControl where the ItemsSource is bound to your collection.
Then add an ItemTemplate that presents the bool via a binding to a checkbox.
You can set the ItemsControl PanelTemplate to a WrapPanel to still get the grid layout look.

Related

View Model Items is Null after updating ItemsSource in User Control

I'm working on a WPF MVVM application and running into an issue. I'm familiar with WPF itself, however I've rarely used MVVM and I suspect I am doing something that MVVM doesn't support, however I don't know how else to accomplish what I am trying to do.
In the application, I have a user control called Agenda. It consist several controls including a text box, a button to add a new agenda item, and a list box with a custom template. The template includes an expander where the header is the agenda item title, up/down arrows to reorder items, and a button to delete the item. The expander content contains a toolbar and a rich text box. In the agenda UC I have a dependency property called ItemsSource which is an IEnumerable<AgendaItem>.
Now, I have a view called Appointment, its associated VM (AppointmentViewModel), and its model (AppointmentModel). In the model, there is a field called AgendaItems which is an ObservableCollection<AgendaItem>. The agenda UC is used within the appointment view and the UC's ItemsSource is bound to the Model.AgendaItems (the observable collection).
The problem I'm having is when I try to handle the buttons to reorder the agenda items in the UC. As an example, for the button to move an agenda item up the list, this is the code in the UC:
var tb = sender as Button;
var tag = tb.Tag as AgendaItem;
var lst = ItemsSource.ToList();
var index = lst.IndexOf(tag);
if(index > 0)
{
lst.RemoveAt(index);
lst.Insert(index - 1, tag);
ItemsSource = lst;
}
The tag of the up arrow is bound to the specific agenda item in the list so I know which item is being moved. The problem comes after I update the ItemsSource property doing ItemsSource = lst. After that line executes, the AgendaItems ObservableCollection in the VM is null. The binding mode is set to TwoWay.
Since the the appointment UC is used in various windows in the application, it made sense to me that the reordering of agenda items should be taken care of my the UC instead of duplicating code in every window which makes use of the UC. But updating the ItemsSource property in the UC results in the collection in the VM being null.
For reference, the ItemsSource property in the UC is defined as:
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<AgendaItem>), typeof(Agenda), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
There is the regular .NET property:
public IEnumerable<AgendaItem> ItemsSource
{
get => (IEnumerable<AgendaItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
And the OnItemsSourceChanged method is:
private static void OnItemsSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is Agenda control)
{
if (e.OldValue is INotifyCollectionChanged oldValueINotifyCollectionChanged)
{
oldValueINotifyCollectionChanged.CollectionChanged -= control.ItemsSource_CollectionChanged;
}
if (e.NewValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
{
newValueINotifyCollectionChanged.CollectionChanged += control.ItemsSource_CollectionChanged;
}
}
}
Any help/guidance on how I can reorder the ItemsSource collection in the UC without breaking the VM would be greatly appreciated. Thank you in advance.
Since calling .ToList() on Enumerable creates new list and reassigning it to ItemsSource doesn't work, maybe define the ItemsSourceProperty as IList<AgendaItem> since this is the interface that you need to reorder the items.
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IList<AgendaItem>), typeof(Agenda), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
(don't forget to change the signature of the ItemsSource property ).
You could also create list extension method to call it in your code like this:
ItemsSource.Move(ItemsSource.IndexOf(tag), MoveDirection.Up);
public static void Move<T>(this IList<T> list, int iIndexToMove,
MoveDirection direction)
{
if (list.Count > 0 && (direction == MoveDirection.Down && iIndexToMove < list.Count - 1)
|| (direction == MoveDirection.Up && iIndexToMove > 0))
{
if (direction == MoveDirection.Up)
{
var old = list[iIndexToMove - 1];
list[iIndexToMove - 1] = list[iIndexToMove];
list[iIndexToMove] = old;
}
else
{
var old = list[iIndexToMove + 1];
list[iIndexToMove + 1] = list[iIndexToMove];
list[iIndexToMove] = old;
}
}
}
public enum MoveDirection
{
Up,
Down
}
By changing the data type of ItemsSource from IEnumerable<AgendaItem> to ObservableCollection<AgendaItem> resolved the issue. Thank you to everyone who responded. It is much appreciated.

defining custom DataTemplates and assign them to the ListView ItemTemplate

I'm trying to get into Xamarin development and followed Microsofts video tutorial
https://www.youtube.com/watch?v=OPXVqdRXZms&list=PLdo4fOcmZ0oU10SXt2W58pu2L0v2dOW-1&index=9
Currently I would like to populate my ListView with some basic labels. So first the following code works fine for me. I created a custom ViewCell and assign it to the ListView as the ItemTemplate
public class MasterPage : ContentPage
{
public ListView MasterPageNavigationItemsView { get; }
public MasterPage()
{
// ...
MasterPageNavigationItemsView = new ListView()
{
ItemTemplate = new DataTemplate(() => new MasterPageItemViewCell()),
SeparatorVisibility = SeparatorVisibility.None
};
MasterPageNavigationItemsView.SetBinding(ListView.ItemsSourceProperty, nameof(MasterViewModel.MasterPageItemsCollection));
// ...
}
}
internal class MasterPageItemViewCell : ViewCell
{
public MasterPageItemViewCell()
{
Label label = new Label();
label.SetBinding(Label.TextProperty, nameof(MasterPageItem.Title));
View = label;
}
}
I would prefer to create a custom DataTemplate as they did in the video tutorial. I found the code on Github
https://github.com/codemillmatt/xamarin-101/blob/8271814c7ebdd41387e20ed33b3dfbdcd54409be/coded-ui-navigation/CodedUINav/Views/MainPage.cs#L88-L112
So instead of doing ItemTemplate = new DataTemplate(() => new MasterPageItemViewCell()), I would like to do ItemTemplate = new MasterPageItemTemplate(),
which results in the class
internal class MasterPageItemTemplate : DataTemplate
{
public MasterPageItemTemplate() : base(LoadTemplate)
{
}
private static Label LoadTemplate()
{
Label titleLabel = new Label();
titleLabel.SetBinding(Label.TextProperty, nameof(MasterPageItem.Title));
return titleLabel;
}
}
So I took the code from Github and modified it a little bit. When I run the code the labels content is empty and when I click on it the application crashes.
How can I fix the MasterPageItemTemplate?
Update
I found another sample that makes use of view cells
https://github.com/xamarin/xamarin-forms-samples/tree/master/WorkingWithListview/WorkingWithListview/Custom
so I think I should follow this and use this code for now
ItemTemplate = new DataTemplate(typeof(MasterPageItemViewCell)),
Data items in a ListView are called cells. Each cell corresponds to a row of data. There are built-in cells to choose from, or you can define your own custom cell. Both built-in and custom cells can be used/defined in XAML or code.The child of an inline DataTemplate must be of, or derive from, type Cell.Here in your first link sample, the ListView.ItemTemplate property is set to a DataTemplate that's created from a custom type that defines the cell appearance. The custom type derive from type ViewCell,but in your codes custom DataTemplate return a Label,it will not work.
you could refer to Creating DataTemplate
And in the sample of your second link,it use CollectionView.You could nest markup inside a DataTemplate tag to create a View.
Note :When using CollectionView, never set the root element of your DataTemplate objects to a ViewCell. This will result in an exception being thrown because CollectionView has no concept of cells.

Is it possible to edit a cell with a CheckBox and TextBlock

I have the following coding where I bind a CheckBox and TextBlock into one DataGridTemplateColumn.
Would it be possible for me to edit the cell with the checkbox and textbox when I click on the cell itself to edit the text inside of it? I still want to be able to set my CheckBox to true or false at the same time as editing the text within the textblock.
Here is my coding:
private void btnFeedbackSelectSupplier_Click(object sender, RoutedEventArgs e)
{
DataGridTemplateColumn columnFeedbackSupplier = new DataGridTemplateColumn();
columnFeedbackSupplier.Header = "Supplier";
columnFeedbackSupplier.CanUserReorder = true;
columnFeedbackSupplier.CanUserResize = true;
columnFeedbackSupplier.IsReadOnly = false;
//My stack panel where I will host the two elements
var stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
DataTemplate cellTemplate = new DataTemplate();
//Where I create my checkbox
FrameworkElementFactory factoryCheck = new FrameworkElementFactory(typeof(CheckBox));
Binding bindCheck = new Binding("TrueFalse");
bindCheck.Mode = BindingMode.TwoWay;
factoryCheck.SetValue(CheckBox.IsCheckedProperty, bindCheck);
stackPanel.AppendChild(factoryCheck);
//Where I create my textblock
FrameworkElementFactory factoryText = new FrameworkElementFactory(typeof(TextBlock));
Binding bindText = new Binding("Supplier");
bindText.Mode = BindingMode.TwoWay;
factoryText.SetValue(TextBlock.TextProperty, bindText);
stackPanel.AppendChild(factoryText);
cellTemplate.VisualTree = stackPanel;
columnFeedbackSupplier.CellTemplate = cellTemplate;
DataGridTextColumn columnFeedbackSupplierItem = new DataGridTextColumn();
columnFeedbackSupplier.Header = (cmbFeedbackSelectSupplier.SelectedItem as DisplayItems).Name;
dgFeedbackAddCost.SelectAll();
IList list = dgFeedbackAddCost.SelectedItems as IList;
IEnumerable<ViewQuoteItemList> items = list.Cast<ViewQuoteItemList>();
var collection = (from i in items
let a = new ViewQuoteItemList { Item = i.Item, Supplier = i.Cost, TrueFalse = false }
select a).ToList();
dgFeedbackSelectSupplier.Columns.Add(columnFeedbackSupplier);
dgFeedbackSelectSupplier.ItemsSource = collection;
}
My example of how it looks now and how I would like to edit that R12 value inside the cell, while still being able to set the checkbox to true or false.
As for my original question, YES you can edit the cell with a CheckBox inside, but instead of a TextBlock I used a TextBox and I changed my the following coding from my question:
var stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);//Delete this line
To
var dockPanel = new FrameworkElementFactory(typeof(DockPanel));
Because a StackPanel does not have support for certain elements (like a TextBox) to fill the remaining available space, where a DockPanel does have support for it.
And then I added this line to make my TextBox fill the remaining space availble
factoryText.SetValue(TextBox.HorizontalAlignmentProperty, HorizontalAlignment.Stretch);
Hope this will help someone else out there :)
Why do you want to use TextBlock instead of TextBox ? If you want to expand the full width of my column length, then just set HorizontalAlignment to Stretch like that:
FrameworkElementFactory factoryText = new FrameworkElementFactory(typeof(TextBox));
factoryText.Text = HorizontalAlignment.Stretch;
Update:
And put your TextBox into Grid or DockPanel; as Zach Johnson says that StackPanel is meant for "stacking" things even outside the visible region, so it won't allow you to fill remaining space in the stacking dimension.

WPB ListBox bind to control inside a class

I have a WPF ListBox of Grids that I create as follows:
I define the listbox in XAML with a simple declaration as follows:
<ListBox Name="MyListbox" >
</ListBox>
In code, I dynamically create an arbitrary number of Grid items (System.Windows.Controls.Grid), and I add them to the ListBox. Something like:
foreach (MyDataType myItem in MyDataList)
{
Grid newGrid = new Grid();
// Code that sets the properties and values of the grid, based on myItem
MyListbox.Items.Add(newGrid);
}
This works great, and everything looks the way that I want it to.
Now, I've decided that in my ListBox, I also want to store a reference to the actual myItem object, so that I can reference it later.
My idea was to create a new class like:
public class ListGridItemNode
{
public MyDataType theItem;
public Grid theGrid;
public ListGridItemNode(MyDataType inItem, Grid inGrid)
{
theItem = inItem;
theGrid = inGrid;
}
}
And then change my code to:
foreach (MyDataType myItem in MyDataList)
{
Grid newGrid = new Grid();
// Code that sets the properties and values of the grid, based on myItem
MyListbox.Items.Add(new ListGridItemNode(myItem,newGrid));
}
Of course, now my listbox instead of displaying the grids, just displays the text "MyApp.ListGridItemNode".
My question is: How do I tell the ListBox to go a level deeper and display the actual Grids inside of each ListGridItemNode object?
I suspect that this has something to do with bindings, but I can't find any examples that work the way that I'm doing it. Most of what I'm finding only shows binding to a string within an object, not an entire control.
Couldn't you just use the Tag property of the Grid object?
newGrid.Tag = myItem;
Then later:
Grid grid; // obtain Grid object somehow
MyItem myItem = (MyItem) grid.Tag;

Binding a WPF DataGrid inside a UserControl

I would like to create a UserControl containing a DataGrid and then define the columns directly inside my UserControl:
<my:ControlContainingDataGrid ItemsSource="{Binding}">
<my:ControlContainingDataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Property1}" Header="Property 1"/>
In the UserControl I expose the columns of the DataGrid :
static ControlContainingDataGrid()
{
ColumnsProperty = DependencyProperty.Register(
"Columns",
typeof(ObservableCollection<DataGridColumn>),
typeof(ControlContainingDataGrid),
new UIPropertyMetadata(new ObservableCollection<DataGridColumn>())
);
}
[Description("Columns"), Category("Columns")]
public ObservableCollection<DataGridColumn> Columns
{
get { return _datagGrid.Columns; }
}
public static readonly DependencyProperty ColumnsProperty;
=> it doesn't work : the column binded to Property1 is not created.
I try to create the column programatically :
_datagGrid.Columns.Add(new DataGridTextColumn {
Header = "Property 1",
Binding = new Binding {
Path = new PropertyPath("Property1"),
Mode = BindingMode.TwoWay,
},
});
_datagGrid.ItemsSource = testList;
=> it doesn't work : the header is displayed but each row of my DataGrid is empty (bad binding ?).
1- What is the simpliest way to bind the columns of a DataGrid via the UserControl in the XAML part ?
2- What is the simpliest way to bind the columns of a DataGrid via the UserControl programatically ?
I do this programmatically. Pretty much the same code as you have. Works fine. I have never tried to bind the Columns collection itself though I don't see why that should not work.
Your message is a little ambiguous. If you mean to say that your column does not show up, maybe you need to add the columns prior to creating the table. If the rows show up with empty value in the column, then your binding 'Property1' is wrong.
I don't work in XAML so don't know nothing about that.

Categories

Resources