I've been using ReactiveUI with WinForms and just did the switch to DynamicData, using a SourceList instead of a ReactiveBindingList.
As per this issue, WinForms IBindingList Collection Support was added.
I have a listbox which I bind to a list of strings. In order to make it work with WinForms, I've created a BindingList which is connected to the SourceList:
var Images = new SourceList<string>();
var ImagesBindableWinForms = new BindingList<string>();
Images.Connect().Bind(ImagesBindableWinForms).Subscribe();
The BindingList is then bound to the listbox as follows, which works swell:
d(this.Bind(ViewModel, x => x.AdInfo.ImagesBindableWinForms, x => x.listImages.DataSource));
There is a button to remove items form the list. It should be disabled if the list is empty. Before switching to SourceList, this used to work:
ViewModel.DeleteImageCmd = ReactiveCommand.Create(DeleteImage, ViewModel.CanDeleteImage());
public IObservable<bool> CanDeleteImage()
{
var canDeleteImage = this.WhenAnyValue(vm => vm.AdInfo.Images.Count)
.Select(x => x > 0);
return canDeleteImage;
}
The code would enable or disable the button depending on the list count.
The same code no longer works. I guess no event is fired when the count is updated.
How would I go about disabling the button if the SourceList is empty?
It is necessary to create an ObservableCollectionExtended and bind that to the SourceList as well. The CanDeleteImage should use that instead of the SourceList, or the BindingList:
var ImagesBindable = new ObservableCollectionExtended<string>();
Images.Connect().Bind(ImagesBindable).Subscribe();
public IObservable<bool> CanDeleteImage()
{
var canDeleteImage = this.WhenAnyValue(vm => vm.AdInfo.ImagesBindable.Count)
.Select(x => x > 0);
return canDeleteImage;
}
Only disadvantage is that I now have three lists, but it works.
Related
This bug exists in the Sample files (StarWarsSample) and I have been trying to remove it for SEVERAL hours... any advice is appreciated:
Using MvvmCross.Forms
I have an MvxListView which has RefreshCommand bound to a command. I have confirmed it fires. When the MvxListView is pulled down (both iOS and Android) a spinner appears. However it never disappears. I do not have any exceptions being thrown. If I raise a dialog it appears over the spinner, but then once cancelled the spinner remains. My ListView is dynamically updating fine. Just the spinner never disappears... I'm assuming it's coming from some specific function in MVVMCross/Forms but i cannot seem to find any reference to it.
Here's the relevant code (in it's current state after trying different approaches)
<views:MvxListView
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
SelectionMode="None"
ItemsSource="{mvx:MvxBind BtDevices}"
ItemClick="{mvx:MvxBind BtDeviceSelectedCommand}"
IsPullToRefreshEnabled="True"
RefreshCommand="{mvx:MvxBind RefreshBtDevicesCommand}"
ItemTemplate="{StaticResource DeviceNameTemplate}"
RowHeight="{x:OnPlatform Android=55, iOS=55, UWP=40}"
BackgroundColor="LightBlue"
SeparatorVisibility="None">
</views:MvxListView>
public HomeViewModel(...){
BtDevices = new MvxObservableCollection<BtDevice>();
BtDeviceSelectedCommand = new MvxAsyncCommand<IBtDevice>(BtDeviceSelected);
FetchBtDevicesCommand = new MvxCommand(() =>
{
FetchBtDeviceTask = MvxNotifyTask.Create(LoadDevices, onException: OnException);
RaisePropertyChanged(() => FetchBtDeviceTask);
});
RefreshBtDevicesCommand = new MvxCommand(()=>
{
RefreshDeviceTask = MvxNotifyTask.Create(RefreshBtDevices, OnException);
RaisePropertyChanged(() => RefreshDeviceTask);
});
}
private async Task RefreshBtDevices()
{
IsBusy = true;
var result = await _btService.LoadDevices(_nextPage);
foreach (var d in result.Where(d => BtDevices.FirstOrDefault(i => i.Id == d.Id) == null))
{
BtDevices.Add(d);
}
IsBusy = false;
await RaisePropertyChanged(() => RefreshDeviceTask);
await RaisePropertyChanged(() => RefreshBtDevicesCommand);
}
From the docs you need to bind the IsRefreshing property of the list view.
Gets or sets a value that tells whether the list view is currently refreshing
Something like this:
IsRefreshing="{mvx:MvxBind IsBusy}"
or if you are using an MvxNotifyTask:
IsRefreshing="{mvx:MvxBind RefreshDeviceTask.IsNotCompleted}"
Here you have more informaion, even though it uses directly ListView on the example the same can be applied to MvxListView given that it inherits from ListView.
HIH
Has simple code in old version of ReactiveUI:
var allItems = new ObservableCollection<Model>(items);
var filteredItems = allItems.CreateDerivedCollection(
x => x,
Filter,
Comparer.Compare);
where Filter and Compare has simple signatures:
private bool Filter(Model item)
public int Compare(Model x, Model y)
sometimes i change items in other threads (big changes, without INPC) or change Filter\Compare strategies and just do filteredItems.Reset();
In DynamicData i try to:
ReadOnlyObservableCollection<Model> filteredItems;
var allItems = new ObservableCollection<Model>(items);
var cancellation = allItems
.ToObservableChangeSet()
.Filter(Filter)
.Sort(Comparer)
.ObserveOn(SynchronizationContext.Current)
.Bind(out filteredItems)
.DisposeMany()
.Subscribe();
but not found, how to Reset this^ or filteredItems.
Wish I were able to give you a direct answer, but I'm rather new to ReactiveUI. Managed to use DynamicData for something and thought you wouldn't mind this sharing.
If what I say later doesn't help you, here are the most relevant resources I could find:
https://floatingcube.com/blog/convert-reactive-list-to-dynamic-data/
https://github.com/reactiveui/DynamicData/wiki/Introduction-for-ReactiveUI-users
https://github.com/reactiveui/DynamicData/issues/182
https://habr.com/en/post/454074/
In my case I used SourceList or SourceCache as type for something similar to your allItems.
SourceCache<Model, string> allItems =
new SourceCache<Model, string>(m => m.Id);
// assuming each model has unique id; if it doesn't then use SourceList
I'd then have a BindingList<Model> as type for something simlar to your 'filteredItems'.
BindingList<Model> filteredItems = new BindingList<Model>();
The binding should be something like:
allItems
.Connect()
...
.ObserveOn(...)
.Bind(filteredItems)
.Subscribe();
To bulk-edit the list I'd call something like
allItems.Edit(
innerList => {
innerList.Clear();
// edit
// innerList.AddOrUpdate(...);
});
Cheers !
I try the new Shell of Xamarin Form 4 for a small project.
I have a list of order, then someone chooses an order and start picking some inventory for this order with barcode. To be simple, I use 2 views and 2 viewmodel.
The process is:
1. User select an order the click a button "pickup"
2. I use ViewModelLocator (TinyIoc) to resolve the correct ViewModel of the pickup view
3. I call Initialize on the new ViewModel and pass some parameters needed. Like a list of items needed to be pickup.
4. Open the view in modal state.
I don't understand that if I change some qty in the list I pass on the second viewmodel, then hit the back button (modal view close). The quantity changed is now in the original viewmodel. How this is possible? I was thinking that passing parameter to a function do not share the same variable but just copy it??
Viewmodel of the first view (look at the Initialize function the List passed and the JobEnCours object)
private async Task GoPickup()
{
Device.BeginInvokeOnMainThread(async () =>
{
if (this.CodeJob != null && this.CodeJob != "")
{
this.IsBusy = true;
PrepChariotSP3ViewModel vm = ViewModelLocator.Resolve<PrepChariotSP3ViewModel>();
await vm.InitializeAsync(this._jobEnCours, this.ComposantesPick.ToList()) ;
await Shell.Current.Navigation.PushAsync(new PrepChariotSP3Page(vm));
this.IsBusy = false;
}
});
}
the the Initialize code on the second Viewmodel (look I set the JobEnCours.Description=Test)
public async Task InitialiseAsync(Job jobEnCours, List<ComposantePick> composantePick)
{
Title = "Ceuillette: " + jobEnCours.CodeProd;
this._jobEnCours = jobEnCours;
this.ComposantesPick = new ItemsChangeObservableCollection<ComposantePick>();
foreach(ComposantePick c in composantePick)
{
this.ComposantesPick.Add(c);
}
jobEnCours.Description = "test";
So, If I do the back button, then in the first VM the JobEnCours.Description is now set to "test" how this is possible?!
Any idea?
Working on Microsoft Dynamics AX 2012.
I have a listpage form which has a referenced ListPageInteraction class, just wanted to change the label / caption of a few control. For this I need to do something like:
element.form().design().control('<YourControlName>');
but I cant get this method on the ListPageInteraction class. I have decided to work on the class's initialized method. However there is no way to get to the form from there, how can I get to the controls and set labels?
common = this.listPage().activeRecord('Table');
if(common.isFormDataSource())
{
fds = common.dataSource();
fds.formRun().control(fds.formRun().controlId('ControlOfScreen')).
userPromptText('New Description');
}
Another example from projProjectTransListPageInteraction.initializeQuery() perspective changing the label of TransDate field from grid on form projProjectTransactionsListPage
public void initializeQuery(Query _query)
{
QueryBuildRange transDateRange;
// ListPageLabelChange =>
Common externalRecord;
FormDataSource frmDs;
FormRun formRun;
FormControl frmCtrl;
// ListPageLabelChange <=
;
queryBuildDataSource = _query.dataSourceTable(tableNum(ProjPostTransView));
transDateRange = SysQuery::findOrCreateRange(queryBuildDataSource, fieldNum(ProjPostTransView, TransDate));
// Date range is [(today's date - 30)..today's date] if not showing transactions for a particular project.
// Date range is [(dateNull())..today's date] if showing transactions for a particular project so that all transactions are visible.
transDateRange.value(SysQuery::range(transStartDate, systemDateGet()));
this.linkActive(_query);
// ListPageLabelChange =>
externalRecord = this.listPage().activeRecord(_query.dataSourceTable(tableNum(ProjPostTransView)).name());//No intrisic function for form DS?
if(externalRecord.isFormDataSource())
{
frmDs = externalRecord.dataSource();
formRun = frmDs.formRun();
if(formRun)
{
frmCtrl = formRun.design().controlName(formControlStr(projProjectTransactionsListPage,TransDate));
if(frmCtrl)
{
frmCtrl.userPromptText("newName");
}
}
}
// ListPageLabelChange <=
}
I don't think it is possible to get the FormRun object from ListPageInteraction.
If you were able to do it the rest would be easy:
FormControl fc = formRun.design().controlName(formcontrolstr(formName, controlName));
// etc.
So this is driving me crazy. I've got an advanced data grid with a dataprovider that's an array collection w/ hierarchical data. Each object (including the children) has an id field. I'm trying to drag and drop data from within the ADG. When this happens, I need to grab the id off the drop target and change the dragged object's parentid field. Here's what I've got:
public function topAccountsGrid_dragDropHandler(event:DragEvent):void{
//In this function, you need to make the move, update the field in salesforce, and refresh the salesforce data...
if(checkActivateAccountManageMode.selected == true) {
var dragObj:Array = event.dragSource.dataForFormat("treeDataGridItems") as Array;
var newParentId:String = event.currentTarget['Id'];
dragObj[0].ParentId = newParentId;
} else {
return;
}
app.wrapper.save(dragObj[0],
new mx.rpc.Responder(
function():void {
refreshData();
},
function():void{_status = "apex error!";}
)
);
}
I can access the data I'm draggin (hence changing parentId) but not the currentTarget. I think the hierarchical data is part of the problem, but I can't find much in the documentation? Any thoughts?
event.currentTarget is not a node, it's the ADG itself. However, it's quite easy to get the information you want, since the ADG stores that data internally (as mx_internal).
I'm using the following code snippets (tested with Flex SDK 4.1) in a dragOver handler, but I guess it will work in a dragDrop handler too.
protected function myGrid_dragDropHandler(event:DragEvent):void
{
// Get the dragged items. This could either be an Array, a Vector or NULL.
var draggedItems:Object = getDraggedItems(event.dragSource);
if (!draggedItems)
return;
// That's our ADG where the event handler is registered.
var dropTarget:AdvancedDataGrid = AdvancedDataGrid(event.currentTarget);
// Get the internal information about the dropTarget from the ADG.
var dropData:Object = mx_internal::dropTarget._dropData;
// In case the dataProvider is hierarchical, get the internal hierarchicalData aka rootModel.
var hierarchicalData:IHierarchicalData = dropTarget.mx_internal::_rootModel;
var targetParent:Object = null;
// If it's a hierarchical data structure and the dropData could be retrieved
// then get the parent node to which the draggedItems are going to be added.
if (hierarchicalData && dropData)
targetParent = dropData.parent;
for each (var draggedItem:Object in draggedItems)
{
// do something with the draggedItem
}
}
protected function getDraggedItems(dragSource:DragSource):Object
{
if (dragSource.hasFormat("treeDataGridItems"))
return dragSource.dataForFormat("treeDataGridItems") as Array;
if (dragSource.hasFormat("items"))
return dragSource.dataForFormat("items") as Array;
if (dragSource.hasFormat("itemsByIndex"))
return dragSource.dataForFormat("itemsByIndex") as Vector.<Object>;
return null;
}
var dropData:Object = mx_internal::dropTarget._dropData;
should be
var dropData:Object = dropTarget.mx_internal::_dropData;
Try this.