1

I have been trying for some hours to get my ListView with ObservableCollection to work. However, no luck. I also read through some posts in here and there and try to match and still no good. Please give me pointer on where it went wrong.

Basically, what I am trying to do is split logic from VM to Helper class. And the logic in this class will update the data but VM does not aware of it.

My problem is in copying file function, the job status does not change the view data. I tried Messenger.Default.Send (from the helper) and Messenger register (in VM), to accept the changes, still no luck.

I am using MVVM Light, WPF, C# by the way.

Here is my code for Model.

public class MyFile : ViewModelBase
{
    public string fullFileName { get; set; }
    public string fileName { get; set; }


    private string _jobStatus;
    public string jobStatus
    {
        get { return _jobStatus; }
        set { Set(ref _jobStatus, value); }
    }
}

Here is my code for Helper.

class FileHelper
{
    public List<MyFile> GetFileName(string dir)
    {
        List<MyFile> lstFiles = new List<MyFile>();

        foreach (var file in (new DirectoryInfo(dir).GetFiles()))
        {
            if (file.Name.ToLower().Contains("xls") && !file.Name.Contains("~$"))
                lstFiles.Add(new MyFile() { fullFileName = file.FullName, fileName = file.Name, jobStatus = "-" });
        }

        return lstFiles;
    }

    public bool CopyFiles(string destDir, List<MyFile> lstFiles)
    {
        try
        {
            int counter = 0;

            foreach (MyFile f in lstFiles)
            {
                f.jobStatus = "Copying";
                File.Copy(f.fullFileName, Path.Combine(destDir, f.fileName),true);
                f.jobStatus = "Finished";
                counter += 1;
                Console.WriteLine("M: " + DateTime.Now.ToString("hh:mm:ss") + "    " + counter);
                Messenger.Default.Send(counter, "MODEL");
            }

            return true;
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: " + e.Message);
            return false;
        }
    }
}

Here is my code for VM.

public class MainViewModel : ViewModelBase
{
    public ICommand CmdJob { get; private set; }



    private ObservableCollection<MyFile> fileList;
    public ObservableCollection<MyFile> FileList
    {
        get { return fileList; }
        set { Set(ref fileList, value); }
    }


    private string counter;
    public string Counter
    {
        get { return counter; }
        set { Set(ref counter, value); }
    }


    public MainViewModel()
    {
        Messenger.Default.Register<int>(this, "MODEL", UpdateCounter);

        CmdJob = new RelayCommand<object>(Action_Job);
        Counter = "0";
    }

    private void UpdateCounter(int bgCounter)
    {
        Counter = bgCounter.ToString();
        RaisePropertyChanged("FileList");
        Console.WriteLine("VM: " + DateTime.Now.ToString("hh:mm:ss") + "    " + Counter);
    }

    private void Action_Job(object tag)
    {
        if (tag == null || string.IsNullOrEmpty(tag.ToString()))
            return;

        switch (tag.ToString())
        {
            case "GET": GetFile();  break;
            case "COPY": CopyFile(); break;
        }
    }


    private void GetFile()
    {
        Counter = "0";
        List<MyFile> myFs = new FileHelper().GetFileName(@"C:\Test\Original\");
        FileList = new ObservableCollection<MyFile>(myFs);
    }

    private void CopyFile()
    {
        if (new FileHelper().CopyFiles(@"C:\Test\Destination\", fileList.ToList()))
            Messenger.Default.Send("Files copying finished", "VM");
        else
            Messenger.Default.Send("Files copying failed", "VM");
    }
}

And here is my XAML.

<ListView Grid.Row="0" Grid.Column="0" ItemsSource="{Binding FileList}" Margin="5,5,0,5" HorizontalAlignment="Left" VerticalAlignment="Stretch" ScrollViewer.VerticalScrollBarVisibility="Visible">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="File Name" Width="170" DisplayMemberBinding="{Binding fileName}" />
                <GridViewColumn Header="Status" Width="170" DisplayMemberBinding="{Binding jobStatus}" >
                </GridViewColumn>
            </GridView>
        </ListView.View>
    </ListView>

    <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Vertical">
        <Button Content="Get file list" Tag="GET" Command="{Binding CmdJob}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" Width="80" Height="25" Margin="0,50,0,50"/>
        <Button Content="Copy file" Tag="COPY" Command="{Binding CmdJob}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Tag}"  Width="80" Height="25" />
        <Label Content="{Binding Counter}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Height="25" FontWeight="Bold" Foreground="Red" Margin="0,50,0,0"/>
    </StackPanel> 

I read through some posts, it said for "change", ObservableCollection will not reflect the changes to View. So, I follow those posts solution (to use Notify change in Model class), not working for me.

For me, my individual file is a big file size, so I can see there is no update on my VM.
If you test with small file size, you will not see the difference.

I tried to use Messenger method, it also not updating on View but my VM can accept incoming message with no issue.

scsfdev
  • 345
  • 1
  • 5
  • 14
  • 1
    Have you tried implementing the INotifyPropertyChanged Interface? – mahlatse Jun 11 '19 at 07:54
  • @mahlatse, I'm using MVVM Light, the "set { Set(ref xxxx}" do the job for me. – scsfdev Jun 11 '19 at 07:56
  • Seems my answer is a bit off then, have you tried this stackoverflow post. https://stackoverflow.com/questions/34243936/propertychanged-wpf-mvvm-light – mahlatse Jun 11 '19 at 07:58
  • But the biggest issue is that your application is not sending or recieving notifications form your class, maybe implement the normal inotifyPropertychnaged interafce and see if you can see the changes and then look too see if maybe your mvvm implementation might be the issue. – mahlatse Jun 11 '19 at 08:00
  • It's unclear what your actual problem is. Are you just asking why you don't see the `jobStatus` update? – Clemens Jun 11 '19 at 08:03
  • @Clemens, yes, the jobStatus is only updating after all job finished. And the counter value too. – scsfdev Jun 11 '19 at 08:07
  • @mahlatse, MVVM Light already cater that job for me. So, I do not need to write such code. If I write those code, might as well not using MVVM Light (please correct me if I am wrong). I already tried Messenger way, it still not working https://stackoverflow.com/questions/16993918/mvvm-light-messenger-sending-and-registering-objects – scsfdev Jun 11 '19 at 08:09
  • 2
    That is because you are setting these properties synchronously in the UI thread. Only the last value will be visible in the UI (when the UI thread has got a chance to update it). Consider making your view model methods `async` and perform an asynchronous file copying, that can be `await`ed. See e.g. here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ – Clemens Jun 11 '19 at 08:12
  • 2
    Also note that it is pointless to use ObservableCollection when you only ever create new collection instances, and never add or remove elements from an existing collection. – Clemens Jun 11 '19 at 08:15
  • @Clemens, thanks for your pointer, I will take a look into this async. To be honest, this is a test project of my real project. In my real project, it has add, remove, update functions (it is not for file copying). – scsfdev Jun 11 '19 at 08:23

2 Answers2

1

You cannot both update the UI and copy files on the same thread simultaneously.

You should either perform the copy on a background thread or use an asynchronous API, e.g.:

public async Task<bool> CopyFiles(string destDir, List<MyFile> lstFiles)
{
    try
    {
        int counter = 0;

        foreach (MyFile f in lstFiles)
        {
            f.jobStatus = "Copying";
            using (Stream source = File.Open(f.fullFileName))
            using (Stream destination = System.IO.File.Create(Path.Combine(destDir, f.fileName)))
                await source.CopyToAsync(destination);
            f.jobStatus = "Finished";
            counter += 1;
            Console.WriteLine("M: " + DateTime.Now.ToString("hh:mm:ss") + "    " + counter);
            Messenger.Default.Send(counter, "MODEL");
        }

        return true;
    }
    catch (Exception e)
    {
        Console.WriteLine("Error: " + e.Message);
        return false;
    }
}

Note that you have to modify the signature of your method to be able to use the async/await keywords that were introduced in the .NET Framework 4.5 and C#5.

You should also await async methods "all the way":

private Task CopyFile()
{
    var fh = new FileHelper();
    if (await fh.CopyFiles(@"C:\Test\Destination\", fileList.ToList()))
        Messenger.Default.Send("Files copying finished", "VM");
    else
        Messenger.Default.Send("Files copying failed", "VM");
}

private async void Action_Job(object tag)
{
    if (tag == null || string.IsNullOrEmpty(tag.ToString()))
        return;

    switch (tag.ToString())
    {
        case "GET": GetFile();  break;
        case "COPY": await CopyFile(); break;
    }
}
mm8
  • 163,881
  • 10
  • 57
  • 88
0

Thanks to Clemens and mm8, I managed to change it to async and await for UI updating.
I assume my way of implementing is still reasonable (compare to mm8).

    private async void CopyFile()
    {
        var fh = new FileHelper();
        bool result = await fh.CopyFilesAsync(@"C:\Test\Destination\", fileList.ToList());

        if (result)
            Messenger.Default.Send("Files copying finished", "VM");
        else
            Messenger.Default.Send("Files copying failed", "VM");
    }

    public async Task<bool> CopyFilesAsync(string destDir, List<MyFile> lstFiles)
    {
        try
        {
            int counter = 0;

            await Task.Run(() =>
            {
                foreach (MyFile f in lstFiles)
                {
                    f.jobStatus = "Copying";
                    Thread.Sleep(500);
                    File.Copy(f.fullFileName, Path.Combine(destDir, f.fileName), true);
                    f.jobStatus = "Finished";
                    counter += 1;
                    Messenger.Default.Send(counter, "MODEL");
                    Thread.Sleep(500);
                }
            });

            return true;
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: " + e.Message);
            return false;
        }
    }
scsfdev
  • 345
  • 1
  • 5
  • 14