Dynamically create TextBoxes and bind them to a list - c#

I am trying to implement a create mask, on which a user can create a new technology with different versions (e.g: .NET with versions 4.5.2 and 4.6 etc.). For that I want the user to be able to dynamically add text boxes for additional versions.
A requirement for this is to use the MVVM Pattern, which i'm fairly new to. I created the following classes for this:
Entitiy Classes (using Entity Framework)
public class Tech : EntityBase
{
// Properties
public string TechName { get; set; }
// Navigation Properties
public virtual ICollection<TechVersion> TechVersions{ get; set; }
}
public class TechVersion : EntityBase
{
// Properties
public string VersionNumber { get; set; }
// Foreign Keys
//public int TechId{ get; set; }
// Navigation Properties
public virtual Tech Tech{ get; set; }
}
Technology View Model
public class TechnologyUpdateViewModel : ObservableObject
{
private string _technologyName;
public string TechnologyName
{
get
{
return _technologyName;
}
set
{
if (_technologyName != value)
{
_technologyName = value;
OnPropertyChanged("TechnologyName");
}
}
}
private ICommand _add;
public ICommand Add
{
get
{
if (_add == null)
{
_add = new RelayCommand(e => CreateTechnology());
}
return _add;
}
set
{
if (_add != value)
{
_add = value;
OnPropertyChanged("Add");
}
}
}
private void CreateTechnology()
{
// Add new Technology from ViewModel Properties
}
}
I am however having trouble with the XAML Code. Binding the Technology Name and adding a new Technology with just the name works fine (Textbox + Button).
But how do I get the dynamically created Textboxes and bind them to the ViewModel?
I've tried the approach I found here
For this I added public ObservableCollection<string> Versions { get; set; }
But most solutions try to create controls from an already filled list. I am trying to create text boxes and bind them to an empty list.
TechnologyAdd.xaml
<UserControl x:Class="Presentation.Views.TechnologyAddOrEdit"
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:local="clr-namespace:Presentation.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<StackPanel MaxWidth="250">
<DockPanel>
<TextBlock>Name: </TextBlock>
<TextBox Text="{Binding TechnologyName}" Margin="10,0,0,0" />
</DockPanel>
<!-- Approach from the Link -->
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<ItemsControl ItemsSource="{Binding Versions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Save" Command="{Binding Add}" />
</StackPanel>
</StackPanel>
Maybe the whole MVVM thing confuses me too much but I can't seem to find a solution for this.
I would be happy about any information on how to approach this correctly.

Related

How to write a DataTemplate that works with a VisualStateGroup in WinUI 3?

I've been trying to pass collections of VisualStateGroup objects to various ContentControls in WinUI 3 (1.1.5) but I keep getting a 'Value does not fall within the expected range.' error. I think I've defined an appropriate DataTemplate, but nothing seems to work. Here's a simple example Page from a new Template Studio Winui 3 project:
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel { get; }
public ShellPage ShellPage { get; }
public MainPage()
{
ViewModel = App.GetService<MainViewModel>();
InitializeComponent();
ShellPage = App.GetService<ShellPage>();
var rootGrid = (ShellPage.IsLoaded) ? VisualTreeHelper.GetChild(ShellPage.NavigationViewControl, 0) as Grid : null;
if (rootGrid != null)
{
var listOfVisualStateGroups = VisualStateManager.GetVisualStateGroups(rootGrid);
if (listOfVisualStateGroups?.Count > 0) cControl.Content = listOfVisualStateGroups[0];
}
}
}
<Page
x:Class="TestCollection.Views.MainPage"
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"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<ContentControl x:Name="cControl">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="VisualStateGroup">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Page>
All this does is fetch the list of VisualStateGroups attached to the NavigationViewControl of a vanilla WinUI 3 project. The first group in the list is then displayed in the ContentControl cControl by assigning it to Content. But the assignment causes the error.
I gather there is something wrong with the DataTemplate, but I can't figure out what. A similar problem occurs if I try to create a templated ListViewItem with a VisualStateGroup object.
This is a significant refinement of a question I asked a few days ago (please see: Bound ListView won't accept List<VisualStateGroup> as ItemsSource in WinUI 3. Any idea why?, if curious). My purpose in asking is to better understand the mechanics and requirements of templating. Any help is greatly appreciated.
Update: 10/2/2022
I haven't found an explanation for the error but I have found that simply wrapping the VisualStateGroup and VisualState classes lets me use collections of the new classes as I would expect. Thing1 and Thing2 are just copies of VisualStateGroup and VisualState (both of which are sealed and can't be inherited directly).
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel { get; }
public ShellPage ShellPage { get; }
public List<Thing1> Things { get; set; } = new();
public List<Thing2> OtherThings { get; set; } = new();
public MainPage()
{
ViewModel = App.GetService<MainViewModel>();
InitializeComponent();
ShellPage = App.GetService<ShellPage>();
var rootGrid = (ShellPage.IsLoaded) ? VisualTreeHelper.GetChild(ShellPage.NavigationViewControl, 0) as Grid : null;
if (rootGrid != null)
{
var listOfVisualStateGroups = VisualStateManager.GetVisualStateGroups(rootGrid);
foreach (var group in listOfVisualStateGroups) Things.Add(new(group));
if (listOfVisualStateGroups?.Count > 0)
foreach (var state in listOfVisualStateGroups[0].States) OtherThings.Add(new(state));
}
}
}
// copy of VisualStateGroup
public partial class Thing1
{
public VisualState CurrentState { get; }
public CoreDispatcher Dispatcher { get; }
public DispatcherQueue DispatcherQueue { get; }
public string Name { get; }
public IList<VisualState> States { get; }
public IList<VisualTransition> Transitions { get; }
public Thing1(VisualStateGroup group)
{
CurrentState = group.CurrentState;
Dispatcher = group.Dispatcher;
DispatcherQueue = group.DispatcherQueue;
Name = group.Name;
States = group.States;
Transitions = group.Transitions;
}
}
// copy of VisualState
public partial class Thing2
{
public CoreDispatcher Dispatcher { get; }
public DispatcherQueue DispatcherQueue { get; }
public string Name { get; }
public SetterBaseCollection Setters { get; }
public IList<StateTriggerBase> StateTriggers { get; }
public Storyboard Storyboard { get; set; }
public Thing2(VisualState state)
{
Dispatcher = state.Dispatcher;
DispatcherQueue = state.DispatcherQueue;
Name = state.Name;
Setters = state.Setters;
StateTriggers = state.StateTriggers;
Storyboard = state.Storyboard;
}
}
<Page
x:Class="TestCollection.Views.MainPage"
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:views="using:TestCollection.Views"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<StackPanel Orientation="Horizontal">
<ListView Margin="0,0,20,20" BorderThickness="1" BorderBrush="Black" Header="Things (VisualStateGroups)"
ItemsSource="{x:Bind Things}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="views:Thing1">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView Margin="0,0,20,20" BorderThickness="1" BorderBrush="Black" Header="OtherThings (VisualStates)"
ItemsSource="{x:Bind OtherThings}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="views:Thing2">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Page>
Any idea why an ObservableCollection<Thing1> works but an ObservableCollection<VisualStateGroup> doesn't?

ItemsControl displaying class name

This app is displaying the class name of a collection instead of a text-box as desired. I've read other issues with this, but cannot figure out what I'm missing. I have a datacontext, I'm bound to the collection as an itemsource, and I've added a single item. All I want is to bind the collection 'Boxes' in my view model 'DrawBoxViewModel' to an item source, and have it display a single item as a text box. All help is appreciated.
First my XAML:
<Page
x:Class="BoxMaker2.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BoxMaker2"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:BoxMaker2.ViewModels"
mc:Ignorable="d">
<Page.Resources>
<vm:DrawBoxViewModel x:Key="DrawBoxViewModel"/>
</Page.Resources>
<Canvas DataContext="{Binding Source={StaticResource DrawBoxViewModel}}">
<ItemsControl ItemsSource="{Binding Boxes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="350" Height="600" Background="AliceBlue"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Resources>
<DataTemplate x:DataType="vm:Box" x:Key="test">
<VariableSizedWrapGrid>
<TextBox Background="White"
Text="{x:Bind Data}"
Width="100"
Height="100"/>
<VariableSizedWrapGrid.RenderTransform>
<TranslateTransform X="{Binding LeftCanvas}" Y="{Binding TopCanvas}"/>
</VariableSizedWrapGrid.RenderTransform>
</VariableSizedWrapGrid>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Canvas>
And now my viewmodel:
namespace BoxMaker2.ViewModels
{
public class DrawBoxViewModel
{
#region fields
private ObservableCollection<Box> _boxes;
#endregion
#region properties
public ObservableCollection<Box> Boxes { get { return this._boxes; } }
#endregion
#region constructors
public DrawBoxViewModel()
{
this._boxes = new ObservableCollection<Box>();
_boxes.Add(new Box() { Data = "hello!", LeftCanvas = 200, TopCanvas = 200 });
}
#endregion
}
public class Box : INotifyPropertyChanged
{
private int _generation;
public int Generation
{
get { return _generation; }
set { _generation = value; OnPropertyChanged("Generation"); }
}
private int _childNo;
public int ChildNo
{
get { return _childNo; }
set { _childNo = value; OnPropertyChanged("ChildNo"); }
}
private Box _parentBox;
public Box ParentBox
{
get { return _parentBox; }
set { _parentBox = value; OnPropertyChanged("ParentBox"); }
}
private List<Box> _childrenBox;
public List<Box> ChildrenBox
{
get { return _childrenBox; }
set { _childrenBox = value; OnPropertyChanged("ChildrenBox"); }
}
private string _data;
public string Data
{
get { return _data; }
set
{
_data = value;
OnPropertyChanged("Data");
}
}
private double _topCanvas;
public double TopCanvas
{
get { return _topCanvas; }
set
{
_topCanvas = value;
OnPropertyChanged("TopCanvas");
}
}
private double _leftCanvas;
public double LeftCanvas
{
get { return _leftCanvas; }
set
{
_leftCanvas = value;
OnPropertyChanged("LeftCanvas");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I am not exactly sure what you are trying to achieve but here's a few issues I have found in your code.
You should assign your VM to the DataContext of the Page
directly.
<Page.DataContext>
<vm:DrawBoxViewModel />
</Page.DataContext>
After doing so, you can now remove DataContext="{Binding
Source={StaticResource DrawBoxViewModel}}" from your Canvas.
Replace <ItemsControl.Resource> with
<ItemsControl.ItemTemplate> and remove x:Key="test", assuming you want to show multiple
TextBoxes on the UI. The DataTemplate within the Resource you
defined won't do anything until you reference it by its key. I don't
think you really want that here though.
You should use x:Bind for your X & Y binding
<TranslateTransform X="{x:Bind LeftCanvas}"
Y="{x:Bind TopCanvas}" />
Your Boxes collection can be simplified as following
#region properties
public ObservableCollection<Box> Boxes { get; } = new ObservableCollection<Box>();
#endregion
#region constructors
public DrawBoxViewModel()
{
Boxes.Add(new Box() { Data = "hello!", LeftCanvas = 0, TopCanvas = 200 });
}
#endregion
Hope this helps!
Your Items control doesn't know which data template to use. Currently your view model has a template associated to it via the x:DataType="vm:Box" which is defined as a resource in the items control.
The problem is that Universal Windows Platform doesn't recognize templates associated to data types. So even though there is a template, the control doesn't know how to find it when it is rendering the collection of view models.
Automatic resolving of templates based on bound types was a function of WPF which is not available in UWP.
What that means is that in WPF you could associate a data template to a class/object via the x:DataType="Object Type" attribute of the data template (which is what you did). When the collection is bound, the rendering engine would auto-magically match the the individual items in the collection to their respective templates.
This was very powerful because if your collection had many different types of boxes for example (or things inheriting from DrawBoxViewModel) you could render each item type differently by simply defining a template. Well this is no more. Microsoft destroyed that feature in UWP.
So long story short - move the template to the page resource collection. Give it a key such as:
<Page.Resources>
<vm:DrawBoxViewModel x:Key="DrawBoxViewModel"/>
<DataTemplate x:Key="test">
<VariableSizedWrapGrid>
<TextBox Background="White"
Text="{x:Bind Data}"
Width="100"
Height="100"/>
<VariableSizedWrapGrid.RenderTransform>
<TranslateTransform X="{Binding LeftCanvas}" Y="{Binding TopCanvas}"/>
</VariableSizedWrapGrid.RenderTransform>
</VariableSizedWrapGrid>
</DataTemplate>
</Page.Resources>
Reference the template in your items control as follows:
<ItemsControl ItemsSource="{Binding Boxes} ItemTemplate={StaticResource test} ">

How do I project models into viewmodels in XAML?

Given the following classes:
public class Neighborhood
{
public IEnumerable<House> Houses { get; set; }
}
public class House
{
public Address Address { get; set; }
public IEnumerable<Room> Rooms { get; set; }
}
public class Room
{
public IEnumerable<Furniture> Furniture { get; set; }
}
I want views that look something like this:
<!-- Want DataContext to be a NeighborhoodViewModel, not a Neighborhood -->
<NeighborhoodView>
<ListBox ItemsSource={Binding Houses}/>
<Button Content="Add House"/>
<Button Content="Remove House"/>
</NeighborhoodView>
<!-- Want DataContext to be a HouseViewModel, not a House-->
<HouseView>
<TextBox Text={Binding Address}/>
<ListBox ItemsSource={Binding Rooms}/>
<Button Content="Add Room"/>
<Button Content="Remove Room"/>
</HouseView>
<!-- Want DataContext to be a RoomViewModel, not a Room -->
<RoomView>
<ListBox ItemsSource={Binding Furniture}/>
<Button Content="Add Furniture"/>
<Button Content="Remove Furniture"/>
</RoomView>
However, I don't want NeighborhoodViewModel to contain HouseViewModels. Rather, it should be like:
public class NeighborhoodViewModel
{
public IEnumerable<Room> Rooms { get; }
public ICommand AddHouseCommand { get; }
public ICommand RemoveHouseCommand { get; }
}
How can I declare bindings to models in XAML, but have the bindings be transformed into viewmodels?
There are two general ways you can create this type of effect. The first way is to create a static view and put a DataContext behind each view. This is not MVVM where the views are generated by the ViewModel but this will get your bindings to work. This is an example of view.
<Grid>
<HouseView x:Name="myHouse">
</HouseView>
</Grid>
In the code behind you can get access to your HouseView and set the data context
public MainWindow()
{
myHouse.DataContext = new MyHouseViewModel();
}
This will get your bindings to work for each one of these controls.
I will say that this is not the best practice of WPF and it is certainly not a way to develop large projects. However this will do for quick and dirty coding in my opinion. You can find out more on how to do proper MVVM style of coding here.

Design Mode Error "Object does not match target type" for Loaded Page Resource

I am currently attempting to mock up data for my view by utilizing Design Data for my ViewModel. Specifically, I have a View front end, and a ViewModel backend for my silverlight application.
When I have mocked up other views, things have worked perfectly. Even in this particular view, the only issue seems to have to be with collections.
Any idea why my "CategoryItem" keeps giving me an error when I try to assign a value to "CategoryName"? I have no idea what is causing the issue...
Code below:
My Design Data:
<vm:MainPageViewModel
xmlns:vm="clr-namespace:WebCatalog.ViewModels"
xmlns:m="clr-namespace:WebCatalog.Models"
SelectedTab="Category 1"
ProjectName="New Project Name"
ShowPopup="False"
IsBusy="False"
CurrentUser="Alex"
>
<vm:MainPageViewModel.Categories>
<m:CategoryItem CategoryName="test"/>
</vm:MainPageViewModel.Categories>
</vm:MainPageViewModel>
My simplified ViewModel:
public class MainPageViewModel {
public string SelectedTab {get;set;}
public string ProjectName {get;set;}
public bool ShowPopup {get;set;}
public bool IsBusy {get;set;}
public string CurrentUser {get;set;}
public ObservableCollection<CategoryItem> Categories {get;private set;}
public MainPageViewModel()
{
Categories = new ObservableCollection<CategoryItem>();
}
}
Finally, my (simplified) view:
<UserControl
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignData Source=../SampleData/MainWindowSampleData.xaml}">
<!-- Decision Categories -->
<StackPanel Width="200" toolkit:DockPanel.Dock="Left" Height="100">
<TextBlock Text="{Binding CurrentUser}">meep</TextBlock>
<ItemsControl Height="100" ItemsSource="{Binding Categories}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>Itemalkdjfa;ldfj;lakdsjfladfjal;dfjaldfja</TextBlock>
<TextBlock Text="{Binding CategoryName}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
On your VM change
public ObservableCollection<CategoryItem> Categories {get;private set;}
to
public ObservableCollection<CategoryItem> Categories {get;set;}
Otherwise my sample xaml binding looked and worked in design mode like this:
<local:MainVM x:Key="myMainVM">
<local:MainVM.Categories>
<local:CategoryItem Name="Test" />
</local:MainVM.Categories>
</local:MainVM>
...
<ListBox DataContext="{StaticResource myMainVM}"
ItemsSource="{Binding Categories}">
<ItemsControl.ItemTemplate>
<DataTemplate><TextBlock Text="{Binding Name}"/></DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
Where the VM mirrored yours (sans the private set) and the VM was loaded in Xaml on Silverlight and showed the data in design mode.
public class MainVM
{
public ObservableCollection<CategoryItem> Categories { get; set; }
public MainVM()
{
Categories = new ObservableCollection<CategoryItem>();
}
}
public class CategoryItem
{
public string Name { get; set; }
}

MVVM Binding Observable Collection to view?

In my application, I need to bind a checkbox list to an observable collection. I have seen many examples but I could not find a proper implementation for this and thats why I am posting this question.
The View:
<Grid Name="GrdMain" Background="White">
<ListView Name="lstConditions" VerticalAlignment="Top" Height="150"
ItemsSource="{Binding ConditionsModels}" Margin="0,25,0,0" BorderBrush="Transparent" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding Path=condition}" Margin="8" Style="{StaticResource CheckBoxDefault}"
IsChecked="{Binding hasCondition,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
</grid>
The model:
public class ConditionsModel
{
public int profileId { get; set; }
public string condition { get; set; }
public bool hasCondition { get; set; }
}
The View Model:
public class ConditionsViewModel : INotifyPropertyChanged
{
private ConditionsModel _conditionsModel;
private ObservableCollection<ConditionsModel> _conditionsModels;
public ConditionsModel ConditionsModel
{
get
{
return _conditionsModel;
}
set
{
_conditionsModel = value;
RaisePropertyChanged("ConditionsModel");
}
}
public ObservableCollection<ConditionsModel> ConditionsModels
{
get
{
return _conditionsModels;
}
set
{
_conditionsModels = value;
RaisePropertyChanged("ConditionsModels");
}
}
public ConditionsViewModel(int profileId)
{
ConditionsModel = new ConditionsModel();
ConditionsModels = new ObservableCollection<ConditionsModel>();
ConditionsModels.CollectionChanged += ConditionsModels_CollectionChanged;
GetConditions(profileId);
}
void ConditionsModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("ConditionsModels");
}
private void GetConditions(int profileId)
{
HealthAssessmentRepository _rep = new HealthAssessmentRepository();
_conditionsModels = _rep.GetConditions(profileId);
}
}
Is this a correct implementation? I need to update the model when the user checks or unchecks the checkbox. But its not raising the propery changed event when the check box is checked or unchecked.Should I implement the INotifyPropertyChanged interface on the model as well?
I have seen many examples, but all of them has different approaches to this and I am confused. Please show the correct implementation of this?
Thanks
I think you have missed the DataType property within DataTemplate. Just refer this
<DataTemplate DataType="{x:Type sampleApp:ConditionsModel}">
Here sampleApp in the namespace reference created within tag. And ConditionsModel is your model class.
You need to implement INotifyPropertyChanged for class ConditionsModel and raise PropertyChangedEvent for the property you want to observe/synchronize, because it is ViewModel as well.
For class ConditionsViewModel, it's the ViewModel of whole ListView, for ConditionsModel, it's the ViewModel of every line. ViewModel can be overlaid. If ConditionsModel is the domain model, my suggestion is that add a new ItemViewModel, because they belong to different layers. It's always better to distinguish the different layers properly.

Categories