I've created a WPF project for the express purpose of illustrating my project.
Disclaimer: I am using Caliburn.Micro ONLY for using PropertyChangedBase and BindableCollection. Everything else here in this project is boilerplate WPF stuff.
Right, so, I have a class DataItem. It has got just two properties:
an int called Qty, and
a calculated property called Sum, which
is double of Qty.
That's it. For the sake of clarity, here's the code:
public class DataLine : PropertyChangedBase
{
private int qty;
public int Qty
{
get
{
return qty;
}
set
{
if (value == qty)
{
return;
}
qty = value;
NotifyOfPropertyChange();
NotifyOfPropertyChange(() => Sum);
}
}
public int Sum => qty * 2;
public DataLine(int a)
{
Qty = a;
}
}
Now, the ViewModel. It's got
a BindableCollection (which is Caliburn's flavour of ObservableCollection) of DataLine,
an int which will add up all the Sum of DataLines, and
an ICommand.
The ICommand does nothing but trigger NotifyPropertyChanged for the BindableCollection and int.
Here's the code:
class ViewModel: PropertyChangedBase
{
public ViewModel()
{
Data = new BindableCollection<DataLine>
{
new DataLine(1),
new DataLine(2),
new DataLine(4),
new DataLine(6)
};
RefreshCommand = new RefreshCommand(this);
}
private BindableCollection<DataLine> _data;
public BindableCollection<DataLine> Data
{
get
{
return _data;
}
set
{
if (value == _data)
{
return;
}
_data = value;
NotifyOfPropertyChange();
NotifyOfPropertyChange(() => Amount);
}
}
public ICommand RefreshCommand { get; private set; }
public void RefreshAction()
{
NotifyOfPropertyChange(() => Data);
NotifyOfPropertyChange(() => Amount);
}
public int? Amount => Data?.Sum(c => c.Sum);
}
Now, for the View:
It's very simple.
There's a DataGrid, bound to the BindableCollection.
There's a Button, bound to the ICommand.
There's a 'TextBox, bound toAmount`.
Here's the body of the View:
<DockPanel>
<TextBox DockPanel.Dock="Bottom" Text="{Binding Amount, TargetNullValue='', Mode=OneWay}"/>
<Button DockPanel.Dock="Bottom" Command="{Binding RefreshCommand}" Content="Refresh"/>
<DataGrid ItemsSource="{Binding Data, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="True"/>
</DockPanel>
When I run it, I get something like this:
Now comes the tricky part - let me just edit one of the rows.
For sake of clarity, I have added an arrow to show where my cursor was after editing the DataGrid. Check how the Sum of first row is updated, but the Amount at the bottom of the screen remains the same.
Now, when I click Refresh, this happens:
The Amount is now updated properly.
All of which finally brings me to my question: What's wrong with DataGrid? Why is DataGrid not triggering its setter when the rows are edited? And how can I make sure it is triggered?
I find Datagrids and updating meta/aggregate data irritating at times.
Currently i would agree with Clemens that i am not sure you can easily change a BindableCollection to suit your needs.
However, a quick solution to not create a tight couple between your DataLine and ViewModel is using a passable Action. You could always get fancy with some abstraction or generics. But I created a simple edit of your code to show what i mean.
Essentially you create a mechanism such that a ViewModel can pass an Action to an Object. If I were implementing this, I would probably implement some form of Interface or Generic that stops me from having to add an Action Method to each Model. Or at least inherit from a class that has the Notify Action. For clarity and easy reading, ill just do a simple edit.
In Your DataLine add:
//NotifyOfPropertyChange();
//NotifyOfPropertyChange(() => Sum);
NotifyFromParent?.Invoke();
to your Qty setter, and:
public DataLine(int a, System.Action action = null)
{
Qty = a;
NotifyFromParent = action;
}
public System.Action NotifyFromParent;
then in your ViewModel you can go:
Data = new BindableCollection<DataLine>
{
new DataLine(1, () => RefreshAction()),
new DataLine(2, () => RefreshAction()),
new DataLine(4, () => RefreshAction()),
new DataLine(6, () => RefreshAction())
};
Like i said, you could make this much fancier. As far as i have experienced you need to create a mechanism to update aggregate or external data when editing cells in a datagrid, and you desire the update to immediately following said edit.
Related
I am trying to implement mvvm and I am struggling with a clean solution to a simple problem. The view can have 3 different states: NewEditable, DisplayOnly, Editable. The idea is that at each state a label and content of a button will change depending on a state, for example, the button will be: "Add", "Edit" and "Save" respectively.
Currently, the vm has bindable properties that update depending on the state of the control but this seems very messy especially when the rest of the logic for the vm is added. Like, it is just playing with strings.
Is there a better, cleaner way? Maybe couple converters that would take a state as input and string as output?
How do you approach changing views based on the state of a vm?
My current ViewModel just for the view logic as you can see loads of boilerplate:
public class ViewModel : NotifyPropertyChangedBase
{
public enum State { New, Edit, DisplayOnly }
public ViewModel()
{
// Set commands
Edit = new CommandHandler(param => EditAction(), () => true);
EndEdit = new CommandHandler(param => EndEditAction(), () => true);
/*
* Some more logic to set up the class
*/
}
public ICommand Edit { get; private set; }
public ICommand EndEdit { get; private set; }
public State DisplayState
{
get { return _displayState; }
private set { SetProperty(ref _displayState, value, nameof(DisplayState)); } // from the base to simply the logic
}
public string ControlTitle
{
get { return _controlTitle; }
private set { SetProperty(ref _controlTitle, value, nameof(ControlTitle)); } // from the base to simply the logic
}
public string ButtonTitle
{
get { return _buttonTitle; }
private set { SetProperty(ref _buttonTitle, value, nameof(ButtonTitle)); } // from the base to simply the logic
}
private State _displayState = State.New;
private string _controlTitle = CONTROL_TITLE_NEW;
private string _buttonTitle = BUTTON_TITLE_NEW;
public const string CONTROL_TITLE_NEW = "New Object"; // Could be removed as used once in the example
public const string CONTROL_TITLE_DISPLAY = "Display Object";
public const string CONTROL_TITLE_EDIT = "Edit Object";
public const string BUTTON_TITLE_NEW = "Create"; // Could be removed as used once in the example
public const string BUTTON_TITLE_DISPLAY = "Edit";
public const string BUTTON_TITLE_EDIT = "Save";
private void EditAction()
{
DisplayState = State.Edit;
ControlTitle = CONTROL_TITLE_EDIT;
ButtonTitle = BUTTON_TITLE_EDIT;
// Some business logic
}
private void EndEditAction()
{
DisplayState = State.DisplayOnly;
ControlTitle = CONTROL_TITLE_DISPLAY;
ButtonTitle = BUTTON_TITLE_DISPLAY;
// Some business logic
}
/*
* Rest of the logic for the class
*/
}
There is multiple approaches to this problem.
Easiest would be to have 2 properties on viewmodel: ButtentText and LabelText. Both returns value which is binded to UI and uses switch inside to select what text it should be.
'More correct' approach would be to have 2 converters, which would basically do same thing: convert enum to some kind display value (button and label). I wouldn't suggest this ways, as first approach is simplier.
Having 3 viewModels for each state looks nice, but just with idea, that in future different things will happen in these viewModels and they will grow apart. If not - it is overkill. And if future will say different, you can always change implementation
I would choose 1st solution, unless your viewModel is +500 rows and it is hard to maintain
If you want to change views depending on viewmodels, I would suggest to read about DataTemplate and DataType. But in this approach would be 1 parent viewModel, which holds what state it should show, and 3 child viewModels. Then you create parentView and inside control with binded current viewModel (one of 3) and with datatypes it will display correct view
I would split your VM into 4, "the common functionality VM" and the 3 for your distinct states, so that you don't have to write switches to change the state. Instead, you would switch between VM's (and possibly between views).
I'm trying to make a listview in xamarin show data from a restapi but have the option to filter the list or sort it based upon last name.
I've set the bindingcontext equal to the apiviewmodel which works. But I want to set the itemssource to a list which can be manipulated later instead of the binding context.
Here is the code that works:
Xaml:
<ListView x:Name="DirectoryListView" ItemsSource="{Binding ContactsList}" IsPullToRefreshEnabled="True">
Xaml.cs:
LocalAPIViewModel = new APIViewModel();
BindingContext = LocalAPIViewModel;
APIViewModel.cs:
private List<MainContacts> _ContactsList { get; set; }
public List<MainContacts> ContactsList
{
get
{
return _ContactsList;
}
set
{
if(value != _ContactsList)
{
_ContactsList = value;
NotifyPropertyChanged();
}
}
}
public class MainContacts
{
public int ID { get; set; }
public string FirstName { get; set; }
}
This all works fine. It's only when I add the following lines that it stops displaying the data in the listview:
xaml.cs:
LocalList = LocalAPIViewModel.ContactsList;
DirectoryListView.ItemsSource = LocalList;
I think I need to add these lines so that I can manipulate the list that's being displayed. Why is the list not being displayed? Is this not how it should be done?
According to your description and code, you use MVVM to bind ListView firstly, it works fine, now you want to use Viewmodel to bind ListView itemsource in xaml.cs directly, am I right?
If yes,I do one sample according to your code, that you can take a look, the data can display successfully.
public partial class Page4 : ContentPage
{
public APIViewModel LocalAPIViewModel { get; set; }
public Page4 ()
{
InitializeComponent ();
LocalAPIViewModel = new APIViewModel();
listview1.ItemsSource = LocalAPIViewModel.ContactsList;
}
}
public class APIViewModel
{
public ObservableCollection<MainContacts> ContactsList { get; set; }
public APIViewModel()
{
loadddata();
}
public void loadddata()
{
ContactsList = new ObservableCollection<MainContacts>();
for(int i=0;i<20;i++)
{
MainContacts p = new MainContacts();
p.ID = i;
p.FirstName = "cherry"+i;
ContactsList.Add(p);
}
}
}
public class MainContacts
{
public int ID { get; set; }
public string FirstName { get; set; }
}
so I suggest you can check ContactsList if has data.
Update:
I want to be able to search the list with a search bar and also order it by first or last names. I also want to be able to click on one of the contacts and open up a separate page about that contact
I do one sample that can meet your requirement, you can take a look:
https://github.com/851265601/xf-listview
So, to answer all your questions...
First, the binding.
Once you set the ItemsSource="{Binding ContactsList}" this means that anytime you signal that you have changed your ContactsList by calling OnPropertyChanged(), that is going to be reflected on the ItemsSource property (so, update the UI - that is why we put the OnPropertyChanged() into the setter). Thus, you do not need to manually set the ItemsSource every time you change it. (Especially from the View, as the View should have no knowledge of how the ContactsList is defined in the ViewModel.)
So you can completely remove those lines from the View's code-behind.
Next, the ordering and searching.
What OnPropertyChanged() does, is that it re-requests the bound property from the ViewModel, and updates the View according to that. So, just after OnPropertyChanged() is called, the getter of the bound property (ContactsList) is called by the View.
So, a good idea is to put the sorting mechanism into the getter of the public property. (Or the setter, when resetting the property.) Something like this:
public class ViewModel {
private ObserveableCollection<MainContacts> contactList { get; set; }
public ObserveableCollection<MainContacts> ContactList {
get {
return new ObservableCollection<MainContacts>(contactList
.Where(yourFilteringFunc)
.OrderBy(yourOrderingFunc));
}
set {
contactsList = value;
OnPropertyChanged();
}
}
//...
}
So, whenever your public property is called, it will sort the private property and return the collection that way.
Change public List<MainContacts> ContactsList to public ObservableCollection<MainContacts> ContactsList
in xaml.cs
instead of LocalList = LocalAPIViewModel.ContactsList;, put
ContactsList = new ObservableCollection(LocalAPIViewModel.ContactsList);
I think this will work, instead of setting ListView's Itemsource to 'LocalList'
I'm building an MVVM Light WPF app in Visual Studio 2015 with Entity Framework 6 (EF) providing the data. I have a ComboBox that displays the reasons why someone needs to take a drug test and it looks like this:
<ComboBox ItemsSource="{Binding ReasonsForTest}"
SelectedItem="{Binding Path=ReasonsForTestVm,
UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Description" />
The ReasonsForTest is of type ReasonForTestViewModel class:
public class ReasonForTestViewModel: ViewModelBase
{
private int _ReasonForTestId;
private string _ReasonForTestAbbr;
private string _description;
public int ReasonForTestId
{
get { return _ReasonForTestId; }
set
{
if (value == _ReasonForTestId) return;
_ReasonForTestId = value;
RaisePropertyChanged();
}
}
public string ReasonForTestAbbr
{
get { return _ReasonForTestAbbr; }
set
{
if (value == _ReasonForTestAbbr) return;
_ReasonForTestAbbr = value;
RaisePropertyChanged();
}
}
public string Description
{
get { return _description; }
set
{
if (value == _description) return;
_description = value;
RaisePropertyChanged();
}
}
}
I have a data service class that contains the following code to fetch the data for the valid values of the ComboBox:
public async Task<ObservableCollection<ReasonForTestViewModel>> GetReasonsForTest()
{
using (var context = new MyEntities())
{
var query = new ObservableCollection<ReasonForTestViewModel>
(from rt in context.ReasonForTests
orderby rt.description
select new ReasonForTestViewModel
{
ReasonForTestId = rt.ReasonForTestID,
ReasonForTestAbbr = rt.ReasonForTestAbbr,
Description = rt.description,
});
return await Task.Run(() => query);
}
}
The view model populates the ComboBox using this:
var dataService = new TestDataService();
ReasonsForTest = await dataService.GetReasonsForTest();
The ComboBox has the correct data; however, it's not selecting the correct value when the app starts -- it's showing blank on load. The SelectedItem (ReasonsForTestVm) is also of that class type ReasonForTestViewModel and gets populated from the database with the one item for this person. I've stepped through the code to ensure ReasonsForTestVm has the correct data, and it does.
Here's the property for ReasonsForTestVm:
public ReasonForTestViewModel ReasonForTestVm
{
get
{
return _reasonForTestVm;
}
set
{
if (Equals(value, _reasonForTestVm)) return;
_reasonForTestVm = value;
RaisePropertyChanged();
}
}
What am I doing wrong here? I'm about to lose my mind!
Update: Sorry for the confusing name in the property above. Fixed.
Any WPF items control that extends Selector (such as ComboBox and ListBox) has two properties that are often used in conjunction: ItemsSource and SelectedItem.
When you bind a collection to ItemsSource, a representation of those items are shown in the UI. Each one of the representations is bound to an instance found within the collection bound to ItemsSource. If, for an example, you're using a DataTemplate to create that representation, you'll find within each that the DataContext will be one of those instances from the collection.
When you select one of these representations, the SelectedItemproperty now holds the instance from the collection that was bound to that representation.
This works perfectly through user interaction with the UI. However, there's one important caveat when interacting with these controls programmatically.
It's a very common pattern to bind these properties to similar properties in your view model.
public class MuhViewModel
{
public MuhItems[] MuhItems {get;} = new[]{ new Item(1), new Item(2) };
// I don't want to show INPC impls in my sample code, kthx
[SuperSlickImplementINotifyPropertyChangedAttribute]
public MuhSelectedItem {get;set;}
}
bound to
<ComboBox ItemsSource="{Binding MuhItems}"
SelectedItem="{Binding MuhSelectedItem}" />
If you try to manually update the selected item this way...
muhViewModel.MuhSelectedItem = new Item(2);
The UI will not change. The Selector sees that ItemsSource has changed, yes, but it doesn't find that instance in the ItemsSource collection. It doesn't know that one instance of Item with a value of 2 is equivalent to any other Item with the same value. So it does nothing. (That's a bit simplistic for what really happens. You can bust out JustDecompile and see for yourself. It gets real convoluted down in there.)
What you should be doing in this situation is updating SelectedItem with an instance found within the collection bound to ItemsSource. In our example,
var derp = muhViewModel.MuhItems.FirstOrDefault(x => x.MuhValue == 2);
muhViewModel.MuhSelectedItem = derp;
Side note, when tracking instances within a debug session, it helps to use Visual Studio's Make Object ID feature.
I'm using an MVVM structure with EF 6.0 as my data source and a Datagrid as my user control.
I have:
Viewmodels: a base that implements INotifyPropertChanged, a main viewmodel that holds other viewmodels and a "concrete" viewmodel where data is displayed from.
Commands: I have an ICommand class and use that in my viewmodel.
In short it so far correctly displays data in a datagrid. Now I want to perform CRUD operations. However when I make changes to the datagrid and then hit my save button (which is bound to an ICommand property) none of the changes propagate back to the entity.
Here is my viewmodel:
class SymbolWeightsViewModel : ViewModelBase
{
BenchMarkEntities _context = new BenchMarkEntities();
RelayCommand _updateCommand;
public ObservableCollection<Weight> Weights
{
get;
private set;
}
public BenchMarkEntities Context
{
get { return _context; }
}
public SymbolWeightsViewModel()
{
_context.Weights.Load();
this.Weights = _context.Weights.Local;
}
~SymbolWeightsViewModel()
{
_context.Dispose();
}
public ICommand UpdateCommand
{
get
{
if (_updateCommand == null)
{
_updateCommand = new RelayCommand(param => this.UpdateCommandExecute(),
param => this.UpdateCommandCanExecute);
}
return _updateCommand;
}
}
void UpdateCommandExecute()
{
using (_context = new BenchMarkEntities())
{
_context.SaveChanges();
}
}
bool UpdateCommandCanExecute
{
get {return true;}
}
My opinion is BenchmarkEntities object and Datagrid are not seeing each other. Why I'm not sure but somewhere that binding is failing. Do I need to reinstantiate BenchmarkEntities to perform an update?
And my View:
<StackPanel>
<DataGrid ItemsSource="{Binding Weights, Mode=TwoWay, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}"
AutoGenerateColumns="True">
</DataGrid>
<Button Name="SaveChanges" Height="32" Command="{Binding UpdateCommand}">
</Button>
</StackPanel>
View related: I think binding the entire Weight object with the DataGrid is probably not going to work. I need to create and bind to individual columns. Is this true or can you perform updates on a part of an entity via a datagrid?
Really appreciate any help or direction.
Thanks
You create a new Context and then SaveChanges on the new Context which has no pending changes:
void UpdateCommandExecute()
{
using (_context = new BenchMarkEntities())
{
_context.SaveChanges();
}
}
As Mike said:
"You create a new Context and then SaveChanges on the new Context which has no pending changes"
It means you shoud not create new context in function UpdateCommandExecute.
So, try modify your UpdateCommandExecute function as following:
void UpdateCommandExecute()
{
_context.SaveChanges();
}
Happy Coding
I've started an MVVM project and now I'm stucking with correct DataBinding.
My project has:
A UserControl whit a ViewModel as DataContext like:
public partial class TestUserControl: UserControl
{
public TestUserControl()
{
this.DataContext = new TestUserControlViewModel();
}
}
ViewModel code is (BaseViewModel class contains PropertyChangedEventHandler):
public class TestUserControlViewModel : BaseViewModel
{
public KrankenkasseControlViewModel()
{}
public IEnumerable<DataItem> GetAllData
{
get
{
IGetTheData src= new DataRepository();
return src.GetData();
}
}
}
IGetTheData is the interface to DataContext:
public interface IGetTheData
{
IEnumerable<DataItem> GetData();
}
}
and finally the DataRepository code:
public class DataRepository : IGetTheData
{
private TestProjectDataContext dax = new TestProjectDataContext();
public IEnumerable<DataItem> GetData()
{
return (from d in this.dax.TestData
select new DataItem
{
ID = d.ID,
SomeOtherData = d.SomeOtherData
});
}
}
My UserControl has a few TextBoxes, but what's the best way to bind correctly?
Thanks for your help, regards.
EDIT: Binding the data against multiple textboxes
After reading your comment, I will elaborate my example for textboxes.
First important thing is that the ViewModel will model the things in the View, so that the View gets all information it needs in the structure it needs. That means, if you have multiple textboses in the View, you will need multiple string Properties in your ViewModel, one for each textbox.
In your XAML you could have something like
<TextBox Text="{Binding ID, Mode=TwoWay}" />
<TextBox Text="{Binding SomeOtherData, Mode=TwoWay}" />
and in your ViewModel
public class TestUserControlViewModel : BaseViewModel {
private string id;
private string someOtherData;
public TestUserControlViewModel() {
DataItem firstItem = new DataRepository().GetData().First();
this.ID = firstItem.ID;
this.SomeOtherData = firstItem.SomeOtherData;
}
public string ID {
get {
return this.id;
}
set {
if (this.id == value) return;
this.id = value;
this.OnPropertyChangedEvent("ID");
}
}
public string SomeOtherData {
get {
return this.someOtherData;
}
set {
if (this.someOtherData == value) return;
this.someOtherData = value;
this.OnPropertyChangedEvent("SomeOtherData");
}
}
}
Here I assume that in your BaseViewModel there is an OnPropertyChangedEvent method to fire the corresponding event. This tells the View that the property has changed and it must update itself.
Note the Mode=TwoWay in the XAML. This means, that it doesn't matter on which side the value changes, the other side will reflect the change immediately. So if the user changes a value in a TwoWay bound TextBox, then the corresponding ViewModel property will automatically change! And also vice versa: if you change the ViewModel property programmatically, the View will refresh.
If you want to show multiple textboxes for more than one data item, then you must introduce more Properties in the ViewModel and bind them accordingly. Maybe a ListBox with a flexible number of TextBoxes inside is a solution then, like #Haspemulator already answered.
Binding the data against a collection control
In the TestUserControl I guess you have a control (like a ListView) to show the list of loaded things. So bind that control against the list in the ViewModel with
<ListView ... ItemsSource="{Binding GetAllData}" ... />
First you must understand that Binding means not "read the data and then forget the ViewModel". Instead you bind the View to the ViewModel (and its Properties) as long as the View lasts. From this point of view, AllData is a much better name than GetAllData (thanks #Malcolm O'Hare).
Now in your code, every time the View reads the AllData property, a new DataRepository is created. Because of the Binding, that is not what you want, instead you want to have one instance of DataRepository for the whole lifetime of the View, which is used to read the initial data and can later be used to update the View, if the underlying database changes (maybe with an event).
To enable such a behavior you should change the type of the AllData property to an ObservableCollection, so that the View can automatically update the list if changes occur.
public class TestUserControlViewModel : BaseViewModel
private ObservableCollection<DataItem> allData;
public TestUserControlViewModel() {
IGetTheData src = new DataRepository();
this.allData = new ObservableCollection<DataItem>(src.GetData());
}
public ObservableCollection<DataItem> AllData {
get {
return this.allData;
}
}
public void AddDataItem(DataItem item) {
this.allData.Add(item);
}
}
Now if you call AddDataItem later, the ListView will update itself automatically.
Your Property Name is bad. You should call it AllData, not GetAllData.
Since you are returning a collection, you probably should be using some sort of list control (ListBox, ListView).
In that case you'd be doing
<ListBox ItemsSource="{Binding GetAllData}" />
Guten Abend. :) As it already mentioned, since you're returning the collection, it's better to use a ListBox. The comment about having ObservableCollection as a cache is also absolutely valid. I would add that if you need to have your data editable, you should use TextBox inside the ItemTemplate:
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text={Binding SomeOtherData,Mode=TwoWay} />
</DataTemplate>
</ListBox.ItemTemplate>
In this case if user edits the text in the box, data will be updated in your data object, so that it could be saved in the database later.