I have a pretty complex question:
At my work, we use WPF with MVVM standards.
On one of our UserControls, there is a treeview that loads all the tables from the database. For each table, the name is loaded in the list by the treeview. When you click on the table name, you can add data to that table from the screen.
Basically, it loads the column names from the table on the side, and lets you enter data and save. The records are then saved and added as children to the table. From here, selecting a child lets you update that info.
Now, the current app loads the data fine. The table names and data are loaded. The child data loads.
Everything is fine, but when it comes to loading the controls, we only use Textboxes.
On the usercontrol the code is:
<UserControl.Resources>
<ResourceDictionary>
<DataTemplate x:Key="AdditionalItemsTemplate">
<Border>
<StackPanel>
<Label
Content="{Binding Name}"
Style="{StaticResource PanelLabelStyle}"/>
<TextBox
Text="{Binding Value}"
Style="{StaticResource TextBoxStyle}"/>
</StackPanel>
</Border>
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<Border
Height="350"
Width="{Binding Width}"
Style="{StaticResource InnerMenuBorderStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border
Grid.Row="0"
Grid.Column="0"
Width="230"
IsEnabled="{Binding IsUpdateEnabled}">
<StackPanel>
<Border
Margin="3,3,3,0"
Style="{StaticResource PanelBorderStyle}">
<StackPanel>
<Label Style="{StaticResource PanelLabelStyle}">
<TextBlock
Text="{Binding TableName}"
TextWrapping="WrapWithOverflow"/>
</Label>
<TextBox
Style="{StaticResource TextBoxStyle}"
Text="{Binding Value}"
TextWrapping="Wrap"/>
<ItemsControl
Width="222"
HorizontalAlignment="Left"
ItemsSource="{Binding additionalFields}" Margin="0,0,-226,0"
ItemTemplate="{StaticResource AdditionalItemsTemplate}"/>
</StackPanel>
</Border>
<Button
Width="70"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="3.5,0,0,0"
Template="{StaticResource UpdateButtonTemplate}"
Command="{Binding ButtonCommand}"
CommandParameter="Update"/>
</StackPanel>
</Border>
<Rectangle
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Width="2"
StrokeDashArray="0.5 1.0 0.3"
Stroke="LightGray"
Visibility="{Binding IsAddVisible}"/>
<Border
Grid.Row="0"
Grid.Column="2"
Width="230"
Visibility="{Binding IsAddVisible}">
<StackPanel>
<Border
Margin="3,3,3,0"
Style="{StaticResource PanelBorderStyle}">
<StackPanel>
<Label
Grid.Row="0"
Content="{Binding LabelText}"
Style="{StaticResource PanelLabelStyle}"/>
<TextBox
Grid.Row="1"
Text="{Binding NewLookup}"
Style="{StaticResource TextBoxStyle}"/>
<ItemsControl
Width="222"
Margin="0,0,-226,0"
HorizontalAlignment="Left"
ItemsSource="{Binding childAdditionalFields}"
ItemTemplate="{StaticResource AdditionalItemsTemplate}"/>
</StackPanel>
</Border>
<Button
Grid.Row="2"
Width="50"
HorizontalAlignment="Left"
Margin="3.5,0,0,0"
Template="{StaticResource AddButtonTemplate}"
Command="{Binding ButtonCommand}"
CommandParameter="Add"/>
</StackPanel>
</Border>
</Grid>
</Border>
In the resources, the AdditionalItemsTemplate loads a label for each column name, and the textbox is loaded with the values, or can be used to enter new values
I would like to make just ONE datepicker for ONE table.
So for example. In table Students, the fields are Name (varchar), Age (varchar), DateStarted (date). A textbox loads for Name and Age, but just for DateStarted a datepicker should load.
So far, in the resources, i add <local:DatePicker
SelectedDate="{Binding Value}"
Visibility="{Binding DateVisible}"/>
So that when the table Student is loaded, the visibility of the datepicker is changed from being hidden to visible.
Here is a sample of the ViewModel:
public AddUpdateConfigurationViewModel(TreeViewContainer treeViewContainer, List<AddChangeSiteConfigurationViewModel> lookupTypeList, FieldDataResponse fieldDataResponse)
{
NewLookup = String.Empty;
Width = 480;
DateVisible = "Hidden";
IsAddVisible = "Hidden";
IsUpdateEnabled = false;
if (treeViewContainer.AdditionalFields != null)
{
if (treeViewContainer.AdditionalFields.Count() > 0)
{
treeViewContainer.AdditionalFields.RemoveAll(x => x.Name.Contains("ID"));
treeViewContainer.AdditionalFields.RemoveAll(x => x.Name.Contains("Guid"));
}
}
if (treeViewContainer.AdditionalFields != null)
this.additionalFields = new ObservableCollection<TreeViewContainer>(treeViewContainer.AdditionalFields);
if ((treeViewContainer.ParentTable == null) | (treeViewContainer.ParentTable == String.Empty))
IsUpdateEnabled = true;
var lookupTypes = lookupTypeList.Where(x => x.Parent_Field == "ID" + treeViewContainer.TableName);
if (treeViewContainer.ParentTable == "IsParent")
{
IsAddVisible = "Visible";
DateVisible = "Hidden";
LabelText = "New " + treeViewContainer.Name;
TableName = treeViewContainer.TableName;
childAdditionalFields = new ObservableCollection<TreeViewContainer>();
foreach (var additionalField in treeViewContainer.AdditionalFields)
childAdditionalFields.Add(new TreeViewContainer(additionalField.Name));
if (treeViewContainer.Name.Equals("Student"))
{
DateVisible = "Visible";
}
}
else if (lookupTypes.Count() > 0)
{
foreach (var lookupType in lookupTypes)
{
IsAddVisible = "Visible";
LabelText = "New " + lookupType.Name;
TableName = lookupType.TableName;
childAdditionalFields = new ObservableCollection<TreeViewContainer>();
foreach (var additionalChildField in lookupType.additionalFieldsDictionary)
childAdditionalFields.Add(new TreeViewContainer(additionalChildField.Key));
}
}
else
Width = 250;
this.Name = treeViewContainer.Name;
this.Value = treeViewContainer.Name;
this.ID = treeViewContainer.ID;
this.treeViewContainer = treeViewContainer;
this.lookupTypeList = lookupTypeList;
this.fieldDataResponse = fieldDataResponse;
}
I hope this makes sense.
Now when i load the usercontrol and select on another table, sometimes the datepicker is visible, even though the visibility is set to Hidden.
And when the Student table is selected, there are datepickers under textboxes for every field.
How do I go about just making one datepicker for this one table?
If there is any more info needed, I will edit and update accordingly
EDIT
Screenshots
This is how the treeview loads. all the tables in the db are listed here
now clicking on a table that shouldn't have the datepicker does this:
it adds the datepicker to this treeview container. i want to eliminate this,
and this is where it should be
but i have too many, see. there should only be one added here. not all the way down, under every textbox.
and the Visible setting is just a temp fix, until i get this datepicker sorted. the visibility can be tackled later
I don't think adding a datepicker and trying to show/hide it is the best way to go. A better way is to load a datatemplate which matches the type which is being bound to. So if the column is a datetime type, you should load a datatemplate which has a label and a datepicker. This is what the DataTemplateSelector is for. You create a class which extends DataTemplateSelector, and decide which template to return, based on the object being bound to.
<UserControl.Resources>
<DataTemplate x:Key="DateTimeFieldTemplate">
<Border>
<StackPanel>
<Label
Content="{Binding Name}"
Style="{StaticResource PanelLabelStyle}"/>
<local:DatePicker
SelectedDate="{Binding Value}"/>
</StackPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="TextFieldTemplate">
<Border>
<StackPanel>
<Label
Content="{Binding Name}"
Style="{StaticResource PanelLabelStyle}"/>
<TextBox
Text="{Binding Value}"
Style="{StaticResource TextBoxStyle}"/>
</StackPanel>
</Border>
</DataTemplate>
<local:TemplateSelector x:Key="TemplateSelector" DateTimeFieldTemplate="{StaticResource DateTimeFieldTemplate}" TextFieldTemplate="{StaticResource TextFieldTemplate}"/>
</UserControl.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Items}" ItemTemplateSelector="{StaticResource TemplateSelector}"/>
</Grid>
You illustrate in your original code that you have a Name and Value property, so you only need to decide on how to provide information on what datatype the Value property is. One way is to make an interface with a Value property of type object:
public interface IBoundDataColumn
{
public string Name { get; } // Not really necessary for the template selector, but perhaps for completeness
public object Value { get; }
}
public class DateTimeColumn : IBoundDataColumn
{
public string Name { get; set; }
public object Value { get; set; }
}
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate TextFieldTemplate
{
get;
set;
}
public DataTemplate DateTimeFieldTemplate
{
get;
set;
}
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
IBoundDataColumn boundCol = item as IBoundDataColumn;
if (boundCol.Value.GetType() == typeof(DateTime))
{
return DateTimeFieldTemplate;
}
else
{
return TextFieldTemplate;
}
return base.SelectTemplate(item, container);
}
}
Or you could have a DataType property:
public interface IBoundDataColumn
{
public Type DataType {get;}
}
public class DateTimeColumn : IBoundDataColumn
{
public string Name {get;set;}
public DateTime Value {get;set;}
public Type DataType { get { return typeof(DateTime); } }
}
Or you could avoid the interface altogether and use reflection to get the type dynamically:
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
var p = item.GetType().GetProperty("Value");
if (t.PropertyType == typeof(DateTime))
...
}
Related
The data is coming through fine, and I can successfully display the data the in a list box, but when i try to do this in a grid view or list view, i get these results. I feel like something small is missing here.
I've set the two way, and update property source on the textboxes, but still no visible data shown. I know it has the data, because it expands like I captured in this screenshot.
LeadingCauseOfDeath Class
public class LeadingCausesOfDeath : BasePropertyHandler
{
public int Year { get; set; }
public string _113_cause_name { get;set;}
public string Cause_name { get; set; }
public string State { get; set; }
public int Deaths { get; set; }
public int AgeAdjustedDeathRate { get; set; }
}
XAML View
<Grid DataContext="{Binding Source={StaticResource HomePageVM}}">
<StackPanel >
<NavigationView PaneDisplayMode="Top" IsBackButtonVisible="Collapsed" >
<NavigationView.MenuItems>
<NavigationViewItem Content="Family"/>
<NavigationViewItem Content="History"/>
<NavigationViewItem Content="About"/>
</NavigationView.MenuItems>
</NavigationView>
<GridView ItemsSource="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsItemClickEnabled="True"
IsSwipeEnabled="true"
SelectionMode="Single"
BorderThickness="10" BorderBrush="Blue">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid MaximumRowsOrColumns="4" Orientation="Vertical"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate>
<Grid BorderBrush="Black" BorderThickness="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Cause_name}" />
<TextBlock Grid.Column="1" Text="{Binding Year}" />
<TextBlock Grid.Column="2" Text="{Binding Deaths}" />
<TextBlock Grid.Column="3" Text="{Binding State}" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
</Grid>
View model
private ObservableCollection<LeadingCausesOfDeath> _data;
public ObservableCollection<LeadingCausesOfDeath> Data
{
get
{
return _data;
}
set
{
_data = value;
OnPropertyChanged("Data");
}
}
public HomePageVM()
{
GetData();
}
private async void GetData()
{
var service = new ServiceForApi();
Data = new ObservableCollection<LeadingCausesOfDeath>();
Data = await service.GetFDAStuff();
OnPropertyChanged("Data");
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged( string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Putting a GridView inside a StackPanel is a bad idea. StackPanel will expand as much as it needs to fit the entire content. So, the automatic virtualization that GridView does is lost because it stretches to fit all the items.
With your ItemsWrapGrid's Orientation set to Vertical, it's probably not expanding at all... it expects a fixed height to fill with items and will expand in the horizontal direction.
Use a Grid with two RowDefinitions... first set to Auto, second set to *
I am new to C# and WPF and still learning the ropes. I am currently trying use a ListBox to display some predefined items in a list. I am using an ObservableCollection to hold those items and I am binding that collection to that ListBox. I am also allowing the user to add new items to the list or update selected ones in addition to deleting them. For each item in that list I want to display a DELETE button beside it. However each button should only be visible for the items that have been added by the user and not any of the predefined items.
I am currently able to display the DELETE button for each item in the list. Therefore my question is, is it possible to set the the property of the DELETE button for each item in the list to be visible only for the items that were newly added to it and have no DELETE buttons showing for the predefined(default) items? If so, how would I go about doing that? (That is what I am struggling to figure out.)
Should I post my code?
Thanks
Here is the viewmode which has the list and the controls to add new items to the list.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox x:Name="DrinksListBox" HorizontalAlignment="Center" Height="325" Width="275" Margin="0,0,0,0" VerticalAlignment="Center" Grid.Column="0">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Type}" Width="80" Margin="0,0,10,0" Grid.Column="0"/>
<TextBlock Text="{Binding Name}" Width="80" Margin="0,0,10,0" Grid.Column="1" HorizontalAlignment="Left"/>
<Button x:Name="DrinkDeleteButton" Content="Delete" Click="CmdDeleteDrink_Clicked" HorizontalAlignment="Right" Grid.Column="2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBox x:Name="DrinkNameTextBox" Grid.Column="1" HorizontalAlignment="Left" Height="45" Margin="0,0,0,100" TextWrapping="Wrap" Text="Enter Drink Name" VerticalAlignment="Center" Width="240" FontSize="20" VerticalContentAlignment="Center"/>
<ComboBox x:Name="DrinkTypeComboBox" Grid.Column="1" HorizontalAlignment="Left" Margin="0,47,0,0" VerticalAlignment="Top" Width="240" Height="45" ItemsSource="{Binding Drinks, Mode=OneWay}" DisplayMemberPath="Type" FontSize="20"/>
<Button x:Name="AddDrinkButton" Content="Add Drink" Grid.Column="1" HorizontalAlignment="Right" Margin="0,0,10,100" VerticalAlignment="Center" Width="100" Height="45" Click="CmdAddDrink_Clicked"/>
</Grid>
Here is my code-behind. I have a inner class for the drink property and the main class that sets up the list to be used.
public partial class MainWindow : Window
{
public ObservableCollection<Drinks> Drinks { get; private set; }
public MainWindow()
{
InitializeComponent();
Drinks = new ObservableCollection<Drinks>();
Drinks.Add(new Drinks("Soda", "Pepsi"));
Drinks.Add(new Drinks("Tea", "Lemon"));
Drinks.Add(new Drinks("Caffinated", "Coffee"));
Drinks.Add(new Drinks("Other", "Water"));
DrinksListBox.ItemsSource = Drinks;
DrinkTypeComboBox.ItemsSource = Drinks;
}
private void CmdDeleteDrink_Clicked(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
if (cmd.DataContext is Drinks deleteDrink)
{
Drinks.Remove(deleteDrink);
}
}
private void CmdAddDrink_Clicked(object sender, RoutedEventArgs e)
{
string typeSelection = ((Drinks)DrinkTypeComboBox.SelectedItem).Type;
Drinks.Add(new Drinks(typeSelection, DrinkNameTextBox.Text));
}
}
Drink class has the type of drink and a name for it.
public class Drinks
{
private string type;
private string name;
public Drinks(string type, string name)
{
this.type = type;
this.name = name;
}
public string Type
{
get { return type; }
set
{
if (type != value)
{
type = value;
}
}
}
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
}
}
}
}
Let's say you have your item:
public class Drinks
{
//your properties, simplified for clarity
public string Name {get;set;}
public string Type {get;set;}
//hey, a new one!
public bool IsUserDefined {get;set;}
}
Then, when the user adds one:
private void CmdAddDrink_Clicked(object sender, RoutedEventArgs e)
{
string typeSelection = ((Drinks)DrinkTypeComboBox.SelectedItem).Type;
Drinks.Add(new Drinks(typeSelection, DrinkNameTextBox.Text)
{
IsUserDefined = true
});
}
disclaimer: from the top of my head; normally that means syntax errors; removed some parts for clarity.
<!-- In your resources section of the XAML -->
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<ListBox x:Name="DrinksListBox" ItemSource="{Binding Drinks}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Type}"/>
<TextBlock Text="{Binding Name}"/>
<Button x:Name="DrinkDeleteButton"
Visibility="{Binding Path=IsUserDefined,
Converter={StaticResource BoolToVis}}"/>
<!-- note: left out some attributes for clarity -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
That should do the trick.
Btw, you seem to be mixing some typical MVVM style coding and code-behind coding. It's worth to say that you might benefit by using a ViewModel in your code.
Let me begin with apologizing for the perhaps somewhat vague title, I'm having a hard time explaining my problem! Perhaps this is why I'm hardly getting any Google results, with this post being the closest to my problem (I think): How to bind to a property of the ViewModel from within a GridView
Anyways, I have a list of news articles that are being dynamically generated. The user has the option to press a "Star"-button in order to add an article to his/her favorites list. This "Star"-button should thus only be visible when the user is logged in.
I'm trying to achieve this by setting the Visibility of the Button to a property called IsLoggedIn inside my ViewModel. However, because this is happening inside my ListView it's trying to find the property IsLoggedIn inside of Article instead of the ViewModel.
So I guess my questions boils down to: How can I bind to a ViewModel property inside of a Databound ListView?
<ListView ItemsSource="{x:Bind VM.Articles, Mode=OneWay}" ItemClick="DebuggableListView_ItemClick" IsItemClickEnabled="True" SelectionMode="None" Grid.Row="1" VerticalAlignment="Top">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Article">
<Grid Margin="0,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="4*"/>
</Grid.ColumnDefinitions>
<Image Source="{Binding Image}" Margin="0,0,10,0" Grid.Column="0" VerticalAlignment="Top" ImageFailed="Image_ImageFailed"/>
<Button Visibility="{x:Bind VM.IsLoggedIn, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}" FontFamily="Segoe MDL2 Assets" Content="" FontSize="30" Background="Transparent" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Bottom"/>
<StackPanel Grid.Column="1">
<TextBlock TextWrapping="Wrap" FontWeight="Bold" Text="{Binding Title}"/>
<TextBlock TextWrapping="Wrap" Text="{Binding Summary}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Requested Article class:
public sealed class Article
{
public int Id { get; set; }
public int Feed { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public DateTime PublishDate { get; set; }
public string Image { get; set; }
public string Url { get; set; }
public string[] Related { get; set; }
public Category[] Categories { get; set; }
public bool IsLiked { get; set; }
}
Okay, so currently I got it working by having a property which gets the singleton of my VM, however I'm sure there has to be a cleaner way to get something simple like this working. I've added a sample rar (OneDrive link: https://1drv.ms/u/s!Ar4fOTiwmGYnyWbwRY6rM0eFsL9x) which has a list, a ViewModel, some dummy data and a Visibility property inside the VM. If you can get it working without my dirty method please feel free to commit as an answer.
This type problem can best be solved using relative binding. Rather than binding directly to the current DataContext, i.e. this case the individual item in the ListView's ItemsSource, you can bind to a property of any ancestor element.
Given a simple ViewModel class
public class ViewModel: ViewModelBase
{
private bool _isLoggedIn;
public bool IsLoggedIn
{
get { return _isLoggedIn; }
set
{
_isLoggedIn = value;
RaisePropertyChanged(() => IsLoggedIn);
}
}
public IEnumerable<string> Items
{ get { return new[] {"One", "Two", "Theee", "Four", "Five"}; } }
}
the View can then be defined as
<Window x:Class="ParentBindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ParentBindingTest"
Title="MainWindow"
Width="300" Height="400">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<CheckBox Margin="16,8" HorizontalAlignment="Left" Content="Logged In?" IsChecked="{Binding IsLoggedIn, Mode=TwoWay}" />
<ListView Grid.Row="1" Margin="16,8" ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Visibility="{Binding Path=DataContext.IsLoggedIn, RelativeSource={RelativeSource AncestorType={x:Type ListView}}, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image Source="Images\remove.png" />
</Button>
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
Note how in the item DataTemplate, the TextBlock's Text property is bound directly to the item.
However the Button's Visibility is bound not to a propery of it's own DataContext, but to that of the ListView itself, using relative binding.
Let's say I have five textboxes and five checkboxes next to them.
Now I want to create x groups of controls like that, then bind it with a list of objects that will contain text from textboxes and data on if checkboxes were checked.
What's the common approach for things like that?
I started with writing a function that would loop x times creating new RowDefinition inside an existing Grid, then inside that Row, make another Grid, inside that Grid create five rows and two columns, and add all those controls, set their rows, columns, add binding etc but I think it can't be the right way to do this, it's too much of a hassle writing all of that. There must be a better way.
You can achieve this functionality using a ItemsControl and Defining Datatemplate for the Items. Refer the below code. I taken an example of 3 TextBoxes and Checkboxes.
<StackPanel>
<ItemsControl x:Name="itms">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox Width="50" Text="{Binding Text1}" Grid.Row="0" Grid.Column="0"/>
<CheckBox IsChecked="{Binding Check1}" Content="Check1" Grid.Row="0" Grid.Column="1"/>
<TextBox Width="50" Text="{Binding Text2}" Grid.Row="1" Grid.Column="0"/>
<CheckBox IsChecked="{Binding Check2}" Content="Check2" Grid.Row="1" Grid.Column="1"/>
<TextBox Width="50" Text="{Binding Text3}" Grid.Row="2" Grid.Column="0"/>
<CheckBox IsChecked="{Binding Check3}" Content="Check3" Grid.Row="2" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Click="Button_Click" Content="Add Items"></Button>
</StackPanel>
public partial class Window2 : Window
{
ObservableCollection<MyClass> lst = new ObservableCollection<MyClass>();
public Window2()
{
InitializeComponent();
itms.ItemsSource = lst;
}
int i = 0;
private void Button_Click(object sender, RoutedEventArgs e)
{
i++;
lst.Add(new MyClass() { Text1 = i.ToString(), Text2 =i.ToString()+1,Text3=i.ToString()+2,Check1=true,Check2=true,Check3=false});
}
}
class MyClass
{
public string Text1 { get; set; }
public string Text2 { get; set; }
public string Text3 { get; set; }
public bool Check1 { get; set; }
public bool Check2 { get; set; }
public bool Check3 { get; set; }
}
To start off, I have a listbox that is trying to accept a UserControl as the DataTemplate:
<ListBox VerticalAlignment="Stretch" Name="GeneralMcmView" Grid.Column="0" HorizontalAlignment="Stretch" >
<ListBox.ItemTemplate>
<DataTemplate DataType="local:GeneralMcmMessage">
<local:GeneralMcmMessage />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
With the contents of the usercontrol looking like:
<ContentControl FontFamily="Segoe UI" VerticalAlignment="Stretch" FontSize="10">
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" Grid.Row="0">
<TextBlock Name="MessageDateTime" Text="{Binding ElementName=_this, Path=TimeStamp, StringFormat=MM/dd/yyyy h:mm:ss.fff tt \'GMT\' (zzz)}" />
<TextBlock Name="MessageTypeLabel" Margin="15,0,5,0" Text="Type:"/>
<TextBlock Name="MessageType" Text="{Binding ElementName=_this, Path=Type}" />
</StackPanel>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" Grid.Row="1">
<TextBlock Name="MessageNameLabel" Margin="0,0,5,0" Text="Message Name:" />
<TextBlock Name="MessageNameValue" Text="{Binding ElementName=_this, Path=MessageName}" TextWrapping="Wrap" />
</StackPanel>
<StackPanel VerticalAlignment="Stretch" Orientation="Vertical" Grid.Row="2">
<TextBlock Name="MessageLabel" Text="Message:"/>
<TextBlock Name="Message" Margin="10,0,0,0" Text="{Binding ElementName=_this, Path=MessageContent}" />
</StackPanel>
</Grid>
</ContentControl>
I then create a couple messages, all with different data (The Listbox's ItemSource is bound to the GeneralMessages ObservableCollection):
GeneralMcmMessage newMsg = new GeneralMcmMessage()
{
MessageId = e.McmMessageViewInfo.Id,
TimeStamp = e.McmMessageViewInfo.MessageDateTime,
Type = e.McmMessageViewInfo.MessageType.ToString(),
MessageName = e.McmMessageViewInfo.MessageName,
MessageContent = e.McmMessageViewInfo.Message.ToString()
};
GeneralMessages.Add( newMsg );
During runtime I interrogate the Items property of the listbox and all the data looks correct, however all I see in the listbox are entries of the GeneralMcmMessage User Control with default data values. Any ideas as to why?
Also, FWIW I am using INotifyPropertyChanged in the usercontrol class:
public partial class GeneralMcmMessage : UserControl, INotifyPropertyChanged
{
private Constants.PiuModule piuModule = Constants.PiuModule.MCM;
private string className = "GeneralMcmMessage";
/// <summary>
/// Event for notifying listeners that a property changed. Part of INotifyPropertyChanged
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public int MessageId { get; set; }
private DateTime timeStamp;
public DateTime TimeStamp
{
get
{
return timeStamp;
}
set
{
timeStamp = value;
OnNotifyPropertyChanged( "TimeStamp" );
}
}
You're saying that the DataTemplate to display a GeneralMcmMessage is to instantiate new GeneralMcmMessage.
<DataTemplate DataType="local:GeneralMcmMessage">
<local:GeneralMcmMessage />
</DataTemplate>
I'd recommend instead of making a collection of your UserControl to make a collection of your model object instead, and bind to the properties within that within your UserControl.
Either way though - the object you created in code will be the DataContext for the one you created in XAML so removing the ElementName=_this should bind appropriately. Try this simplified XAML in your UserControl instead.
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" Grid.Row="0">
<TextBlock Name="MessageDateTime" Text="{Binding TimeStamp, StringFormat=MM/dd/yyyy h:mm:ss.fff tt \'GMT\' (zzz)}" />
<TextBlock Name="MessageTypeLabel" Margin="15,0,5,0" Text="Type:"/>
<TextBlock Name="MessageType" Text="{Binding Type}" />
</StackPanel>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" Grid.Row="1">
<TextBlock Name="MessageNameLabel" Margin="0,0,5,0" Text="Message Name:" />
<TextBlock Name="MessageNameValue" Text="{Binding MessageName}" TextWrapping="Wrap" />
</StackPanel>
<StackPanel VerticalAlignment="Stretch" Orientation="Vertical" Grid.Row="2">
<TextBlock Name="MessageLabel" Text="Message:"/>
<TextBlock Name="Message" Margin="10,0,0,0" Text="{Binding MessageContent}" />
</StackPanel>
You don’t post all code of GeneralMcmMessage hence I don’t know if you set DataContext in user control for example in constructor of GeneralMcmMessage.
I tried replicate your problem.
User control GeneralMcmMessage code behind
public partial class GeneralMcmMessage : UserControl, INotifyPropertyChanged
{
private int _messageId;
public int MessageId
{
get
{
return _messageId;
}
set
{
_messageId = value;
OnPropertyChanged("MessageId");
}
}
private DateTime _timeStamp;
public DateTime TimeStamp
{
get
{
return _timeStamp;
}
set
{
_timeStamp = value;
OnPropertyChanged("TimeStamp");
}
}
public GeneralMcmMessage()
{
InitializeComponent();
//don’t set data context here
//DataContext = this;
}
}
User control GeneralMcmMessage XAML
<StackPanel>
<TextBlock Margin="5" FontSize="20" Text="{Binding Path=MessageId}"/>
<TextBlock Margin="5" FontSize="20" Text="{Binding Path=TimeStamp}"/>
</StackPanel>
User control GeneralMcmMessage usage
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ObservableCollection<GeneralMcmMessage> _generalMessages;
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public ObservableCollection<GeneralMcmMessage> GeneralMcmMessages
{
get { return _generalMessages; }
set
{
_generalMessages = value;
OnPropertyChanged("GeneralMcmMessages");
}
}
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
{
GeneralMcmMessages = new ObservableCollection<GeneralMcmMessage>();
for (int i = 0; i < 10; i++)
{
var newMsg = new GeneralMcmMessage
{
MessageId = i,
TimeStamp = DateTime.Now,
};
GeneralMcmMessages.Add(newMsg);
}
}
}
User control GeneralMcmMessage usage XAML
<ListBox x:Name="ListBox"
Margin="5"
ItemsSource="{Binding Path=GeneralMcmMessages}">
<ListBox.ItemTemplate>
<DataTemplate DataType="stackoverflow:GeneralMcmMessage">
<stackoverflow:GeneralMcmMessage/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
If this not solve your problem could you please post all GeneralMcmMessage?
If you want I can upload sample project for you.
Thank you