I have a screen displaying a list of items on which the user can click a button to remove the corresponding item from the list.
I am trying to do so using MVVM.
But the item is not aware of the containing list when it gets the action.
I saw some answers here and there, but none of them using out of the box MVVM features I have in my environment
For example that one using PRISM (don't know if I should use that too, is it standard?):
How to properly remove Items from a ListView when the ItemTemplate is a User Control?
Here is the XAML:
<ListView ItemsSource="{Binding MyItemList}" SelectionMode="None" ScrollViewer.VerticalScrollMode="Disabled" ItemContainerTransitions="{x:Null}">
<ListView.ItemTemplate>
<DataTemplate >
<Grid Grid.Row="1" HorizontalAlignment="Stretch" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding ItemClass.Property01, Mode=TwoWay}" />
<Button Grid.Column="1" Command="{Binding RemoveItemCommand}" >
<SymbolIcon Symbol="Cancel" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And here is the ModelView list:
private static ObservableCollection<ItemClass> _MyItemList = new ObservableCollection<ItemClass> {
new ItemClass{ Property01 = "Sometext" }
};
public ObservableCollection<ItemClass> MyItemList { get { return _MyItemList; } }
And I want to be able to perform the following (the example of code from the main model view, I could create an item model view if necessary for solving):
public IMvxCommand RemoveItemCommand { get; private set; }
public MyViewModel(IUserDialogs dialogs)
{
RemoveItemCommand = new MvxCommand(RemoveItem);
}
public void RemoveItem(object theItem) { MyItemList.Remove(theItem); }
Add x:Name="listView" attribute to your ListView, then in the template
<Button Grid.Column="1"
Command="{Binding ElementName=listView, Path=DataContext.RemoveItemCommand}"
CommandParameter="{Binding}" >
However, when I face problems like this, I usually just use code behind instead. The reason for that, I can use debugger for C# code in visual studio, but debugging these complex bindings is much harder. Here’s a C# version, the code is IMO cleaner, and easier to debug:
void removeItem_Click( object sender, RoutedEventArgs e )
{
object i = ((FrameworkElement)sender).DataContext;
( this.DataContext as MyViewModel )?.RemoveItem( i );
}
Or maybe that's just my personal preference.
It would be better to have a context menu item on the list view (or a delete button on the page somewhere) to delete the currently selected item(s). You can then get the selection from the list view.
Alternatively you could attach the context menu to the list view item in PrepareContainterForItemOverride (and detach it in the other Override method)
That would be a more standards interaction style.
If you must have the button inside the list view item, then the easiest way to get the list item would probably be to use a visual tree helper to go up from the button to the list view item and then get the actual item from the list view item.
Thanks for all the hints,
Using Soonts answer, I was able to develop a fast solution,
Here is what the final implementation looks like for reference for whoever wants to copy/paste/adapt (note I did not test code as I replaced variables/functions names):
XAML:
<ListView x:Name="ItemClass_ListView" ItemsSource="{Binding MyItemList}" SelectionMode="None" ScrollViewer.VerticalScrollMode="Disabled" ItemContainerTransitions="{x:Null}">
<ListView.ItemTemplate>
<DataTemplate >
<Grid Grid.Row="1" HorizontalAlignment="Stretch" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding ItemClass.Property01, Mode=TwoWay}" />
<Button Grid.Column="1" Command="{Binding ElementName=ItemClass_ListView, Path=DataContext.RemoveItemCommand}" CommandParameter="{Binding}" >
<SymbolIcon Symbol="Cancel" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ViewModel:
public class MyViewModel : BaseViewModel, INotifyPropertyChanged
{
public IMvxCommand RemoveItemCommand { get; private set; }
public MyViewModel()
{
// Initializing Commands
RemoveItemCommand = new MvxCommand<ItemClass>(OnRemoveItemClick);
}
public void OnRemoveItemClick(ItemClass anItem)
{
// Do stuff...
}
private static ObservableCollection<ItemClass> _MyItemList = new ObservableCollection<ItemClass> {
new ItemClass(),
new ItemClass()
};
public ObservableCollection<ItemClass> MyItemList
{
get { return _MyItemList; }
}
}
Related
I have added the template based on this link
I have an Add button - when I click on it through Command I add it to a collection.
<ItemsControl ItemsSource="{Binding Collection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid DataContext="{StaticResource VieWModel}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15*"/>
<ColumnDefinition Width="40*"/>
</Grid.ColumnDefinitions>
<Label Content="GH" Grid.Row="0" Grid.Column="0" VerticalContentAlignment="Center"></Label>
<tk:RadComboBox Grid.Row="0" Grid.Column="0" Margin="10" IsFilteringEnabled="True" Width="150" DisplayMemberPath="D" IsEditable="True" ItemsSource="{Binding GK}" SelectedItem="{Binding SK, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" CommandParameter="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</tk:RadComboBox>
<Label Content="HB" Grid.Row="0" Grid.Column="1" VerticalContentAlignment="Center"></Label>
<tk:RadComboBox Grid.Row="0" Grid.Column="1" Margin="10" IsFilteringEnabled="True" Name="cb" Width="350" IsEditable="True" DisplayMemberPath="D" ItemsSource="{Binding VR}" SelectedItem="{Binding VR1,Mode=TwoWay}">
</tk:RadComboBox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
ViewModel sample code:
// Property for selected Item in combox1
Public ValBase SK{get;set;}
//Property off combobox1 binding
Public ValBase GK{get;set;}
// Property ofor selected Item in combox2
Public ValBase VR1{get; set;}
//Property ofr combobox2 binding
Public ValBase VR{get;set;}
Public void AddButton(object obj)
{
var item =new collectionbase();
Collection.Add(item)
}
Whenever I click the Add Button this itemplate will be added.
MyRequirement :
When I Click Add Button for the first time ,template should get added
When I click Add Button for the second time Previous generated controls Must have contain the values,only then controls should be added to a collection and then new controls should be created
And I dont know how to save those values dynamically created in a collection
I am running out of Ideas how to achieve this can anyone help . MVVM pattern
I guess you are having Collection in MainViewModel and Command for add Model.
private Model _lastAdded;
public Model LastAdded
{
get{return _lastAdded;}
set{_lastAdded = value;}
}
private void AddCommand(object obj)
{
if(_lastAdded != null && _lastAdded.SelectedValue != null)
{
var newItem = new Model();
Collection.Add(newItem);
_lastAdded = newItem;
}
else
{
//Show message
}
}
Using Selector Class.I have used CurrentInstance To Bind the Collection its working fine now
I am working on a local project and I have some issues.
I want to create a template for some results that have 3 strings(where 1 is a hyperlink) and a picture and they come as an ObservableCollection of results type binded to ItemSource.
public TestClass {
public string Title { get; set; }
public string Description { get; set; }
public string Link { get; set; }
public BitmapImage Thumbnail { get; set; }
}
So, I want to show those results in WPF and I want to use for each item a template and show them in a StackPanel (or ListView).
I tried with ListView but the only thing you can do is select the whole item, but I want also the link to be clickable.
My problem is: how can I create a template to use on each item and then add them in a list that 1 string is clickable?
As Unflux mentioned, that's a good way to do it. And as for the clickable link, use the Hyperlink control like I did below.
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding FirstName}" Grid.Row="0" Grid.Column="0" />
<TextBlock Text="{Binding LastName}" Grid.Row="0" Grid.Column="1" />
<TextBlock Text="{Binding Age}" Grid.Row="0" Grid.Column="2" />
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
<Hyperlink NavigateUri="{Binding BlogAddress}" Click="Hyperlink_OnClick">
<TextBlock Text="{Binding BlogAddress}" />
</Hyperlink>
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
and code-behind
private void Hyperlink_OnClick(object sender, RoutedEventArgs e)
{
var link = sender as Hyperlink;
Process.Start(link.NavigateUri.ToString());
}
results in
You will probably want to style it a bit and maybe apply a different ItemsPanel to really customize the look of your collection. You can also decorate ItemsControl with scrolling.
Thanks with the idea of using ItemsControl with its template. But the Hyperlink I made it work with Click property and giving to it a:
public ICommand RunHyperlink {
get {
return new ActionCommand(this.ButtonClick);
}
}
private void ButtonClick() {
Process.Start(new ProcessStartInfo(this.Link));
}
The Problem
I have a C# window with some text fields and buttons on it. It starts out similar to this:
When the user clicks that "+ Add Machine Function" button, I need to create a new row of controls and move the button below those:
If the user clicks "+Add Scale Unit" the program needs to add some controls to the right:
Attempts at a solution
I have tried using Windows Forms' TableLayoutPanel but it seemed to handle resizing itself to fit additional controls in odd ways, for example it would some one rows of controls much wider than the others, and would make some rows so short it cut off parts of my controls.
I have also tried simply placing the controls by themselves into the form by simply calculating their relative positions. However I feel that this is bad programming practice as it makes the layout of the form relatively hard to change later. In the case of the user deleting the row or scale unit by pressing the 'X' beside it, this method also requires the program to find each element below that one and move it up individually which is terribly inefficient.
My question is: how would I go about creating a dynamically growing/shrinking application, either through Windows Forms layouts or WPF or something else?
In WPF you can do this:
Classes
public class MachineFunction
{
public string Name { get; set; }
public int Machines { get; set; }
public ObservableCollection<ScaleUnit> ScaleUnits { get; set; }
public MachineFunction()
{
ScaleUnits = new ObservableCollection<ScaleUnit>();
}
}
public class ScaleUnit
{
public string Name { get; set; }
public int Index { get; set; }
public ScaleUnit(int index)
{
this.Index = index;
}
}
Window.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ItemsControl Name="lstMachineFunctions">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="1" Text="Machine Function"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="Number of Machines"/>
<Button Grid.Row="1" Grid.Column="0" Click="OnDeleteMachineFunction">X</Button>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Name}"/>
<TextBox Grid.Row="1" Grid.Column="2" Text="{Binding Machines}"/>
</Grid>
<ItemsControl ItemsSource="{Binding ScaleUnits}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="12,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="1" Text="Machine/Scale Unit"/>
<Button Grid.Row="1" Grid.Column="0" Click="OnDeleteScaleUnit">X</Button>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Name}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Index, StringFormat='Scale Unit {0}'}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<Button VerticalAlignment="Center" Click="OnAddScaleUnit">Add Scale Unit</Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button HorizontalAlignment="Left" Click="OnAddMachineFunction">Add Machine Function</Button>
</StackPanel>
</Window>
Window.cs
public partial class MainWindow : Window
{
public ObservableCollection<MachineFunction> MachineFunctions { get; set; }
public MainWindow()
{
InitializeComponent();
lstMachineFunctions.ItemsSource = MachineFunctions = new ObservableCollection<MachineFunction>();
}
private void OnDeleteMachineFunction(object sender, RoutedEventArgs e)
{
MachineFunctions.Remove((sender as FrameworkElement).DataContext as MachineFunction);
}
private void OnAddMachineFunction(object sender, RoutedEventArgs e)
{
MachineFunctions.Add(new MachineFunction());
}
private void OnAddScaleUnit(object sender, RoutedEventArgs e)
{
var mf = (sender as FrameworkElement).DataContext as MachineFunction;
mf.ScaleUnits.Add(new ScaleUnit(mf.ScaleUnits.Count));
}
private void OnDeleteScaleUnit(object sender, RoutedEventArgs e)
{
var delScaleUnit = (sender as FrameworkElement).DataContext as ScaleUnit;
var mf = MachineFunctions.FirstOrDefault(_ => _.ScaleUnits.Contains(delScaleUnit));
if( mf != null )
{
mf.ScaleUnits.Remove(delScaleUnit);
foreach (var scaleUnit in mf.ScaleUnits)
{
scaleUnit.Index = mf.ScaleUnits.IndexOf(scaleUnit);
}
}
}
}
I did the same thing recently in WinForms and the way I did it was as follows:
Create a UserControl that contains the controls I wanted to repeat
Add a FlowLayoutPanel to the main form to contain all the user controls (and to simplify their positioning)
Add a new instance of your custom UserControl to the FlowLayoutPanel every time you want a new "row" of controls
flowLayoutPanel1.Controls.Add(
new MachineFunctionUC {
Parent = flowLayoutPanel1
});
To remove a row of control call this.Dispose(); from within the user control (that's the instruction executed by the "X" button).
If you want the UserControls to be arranged vertically set the following properties:
flowLayoutPanel1.AutoScroll = true;
flowLayoutPanel1.WrapContents = false;
flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
And to access them use flowLayoutPanel1.Controls[..]
The correct way to achieve your requirements in WPF is for you to define a custom data type class to represent your machine function. Provide it with how ever many properties that you need to represent your machine fields. When you have done this, you then need to move the code that generated your machine function UI into a DataTemplate for the type of your class and data bind all of the relevant properties:
<DataTemplate DataType="{Binding YourPrefix:MachineFunction}">
...
</DataTemplate>
Then, you need to create a collection property to hold your machine function items and data bind that to some kind of collection control. Once you have done this, then to add another row, you just need to add another item to the collection:
<ItemsControl ItemsSource="{Binding MachineFunctions}">
<ItemsControl.Resources>
<DataTemplate DataType="{Binding YourPrefix:MachineFunction}">
...
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
<Button Content="+ Add Machine Function" ... />
...
MachineFunctions.Add(new MachineFunction());
Please see the Data Binding Overview page on MSDN for further help with data binding.
Create a function which will define a row for you. Consider the code and use its where to place another control and do as for buttons also and count it position.
Button button1=new Button();
button1.Text="dynamic button";
button1.Left=10; button1.Top=10; //the button's location
this.Controls.Add(button1); //this is how you can add control
My project is utilizing MVVM with C#. I've bounded my button command to a RelayCommand, and I wish to get information about my button. I wish to get this information so that I can use it in my RelayCommand. Unfortunately I do not know how to send this information to my RelayCommand, nor do I know which EventArgs I need to receive in my RelayCommand to get this Information.
<ListBox ItemsSource="{Binding Decoration}" x:Name="MyLB">
<ListBox.ItemTemplate>
<DataTemplate>
<Button BorderBrush="Transparent" BorderThickness="0" Command="{Binding DataContext.AddGearCommand, ElementName=MyLB}" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<View:ShielGear/>
</Grid>
<TextBlock Text="HEJ MED DIG LUDER" TextWrapping="Wrap" Grid.Column="1"/>
</Grid>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The ShielGear contains a Path element which the button takes it shape after. The RelayCommand I've bounded the command to is:
AddGearCommand = new RelayCommand<T>(addGear);
private void addGear(T e)
{
}
Furthermore is it possible to parse more than one Type to the relaycommand?
I am also unsure if I should use Commandparameters?
You shouldn't be accessing the button (a UI element) from the ViewModel. This is breaking the separation of concerns and will make life difficult for you later if you need to refactor the UI.
Instead, add a value to the buttons binding which will pass the data you need into the command. Often, this will be the object that is bound to your listboxitem.
<Button Command="{Binding DataContext.AddGearCommand, ElementName=MyLB}" CommandParameter="{Binding}">
Then you need to modify your RelayCommand to be typed with the actual type of your data element.
public RelayCommand<myDataType> AddGearCommand { get;set;}
If you add a name to your ListBox you can use CommandParameter to send the SelectedIndex as a parameter
<ListBox x:Name="myListBox" ...>
In your command
<Button BorderBrush="Transparent" BorderThickness="0" Command="{Binding DataContext.AddGearCommand, ElementName=MyLB}" CommandParameter="{Binding ElementName=myListBox, Path=SelectedIndex}">
Then, your RelayCommand decleration will be as follows:
public RelayCommand<int> AddGearCommand { get; set; }
And in your command :
AddGearCommand = new RelayCommand<int>(selectedIndex =>
{
// do whatever you want
});
Hope this helps
Pass your button name in Commandparameter and in viewmodel cast your parameter as button.
now you can get all info of your button.
XAML:
<Button x:Name="btnPrint" MinWidth="70" Margin="5" Content="Print"
Command="{Binding Print}" CommandParameter="{Binding ElementName=btnPrint}" ></Button>
ViewModel:
private RelayCommand _commandPrint;
public ICommand Print
{
get { return _commandPrint ?? (_commandPrint = new RelayCommand(param => this.PrintGrid(param), Canprint)); }
}
private void PrintGrid(object param)
{
var btn = param as Button;
}
I develop an app for Windows Phone 7 with using of Caliburn Micro and Reactive Extensions.
The app has a page with a ListBox control:
<Grid x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Views:ItemView Margin="0,12,0,0" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
I'm using the next ItemView as a DataTemplate:
<UserControl ...>
<Grid x:Name="LayoutRoot"
cal:Message.Attach="[Event Tap] = [Action SelectItem]">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Style="{StaticResource PhoneTextLargeStyle}"
Text="{Binding Name}"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Foreground="{StaticResource PhoneDisabledBrush}"
Style="{StaticResource PhoneTextLargeStyle}"
Text="{Binding Id}" />
</Grid>
</UserControl>
And the corresponding ItemViewModel looks like this:
public class ItemViewModel
{
private readonly INavigationService _navigationService;
public int Id { get; private set; }
public string Name { get; private set; }
public ItemViewModel(Item item)
{
Id = item.Id;
Name = item.Name;
_navigationService = IoC.Get<INavigationService>();
}
public void SelectItem()
{
_navigationService.UriFor<MainViewModel>()
.WithParam(x => x.Id, Id)
.Navigate();
}
}
}
The ListBox populates with items:
public class ListViewModel : Screen
{
private readonly IItemsManager _itemsManager;
private List<ItemViewModel> _items;
public List<ItemViewModel> Items
{
get { return _items; }
private set
{
_items = value;
NotifyOfPropertyChange(() => Items);
}
}
public ListViewModel(IItemsManager itemsManager)
{
_itemsManager = itemsManager;
}
protected override void OnViewReady(object view)
{
base.OnViewReady(view);
Items = null;
var list = new List<ItemViewModel>();
_itemsManager.GetAll()
.SubscribeOn(ThreadPoolScheduler.Instance)
.ObserveOnDispatcher()
.Subscribe((item) => list.Add(new ItemViewModel(item)),
(ex) => Debug.WriteLine("Error: " + ex.Message),
() =>
{
Items = list;
Debug.WriteLine("Completed"));
}
}
}
And here the problems begin.
_itemsManager returns all items correctly. And all items correctly displayed in the ListBox. There is ~150 items.
When I tap on an item then SelectItem method in the corresponding ItemViewModel must be called. And all works fine for first 10-20 items in ListBox. But for all the next items SelectItem method is called in absolutely incorrect ItemViewModel. For example, I tap on item 34 and SelectItem method is called for item 2, I tap 45 - method is called for item 23, and so on. And there is no no dependence between items.
I already head breaks in search of bugs. In what could be the problem?
The solution was found after reading the discussion forum and the page in documentation of Caliburn.Micro.
All problems were because of Caliburn.Micro's Conventions.
To solve the problem I've added to the DataTempalate the next code: cal:View.Model={Binding}. Now part of the page with the ListBox looks like this:
<Grid x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Views:ItemView Margin="0,12,0,0" cal:View.Model={Binding}/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
I think it's not a perfect answer. So I'll be glad if someone can provide better answer and explanation.