Xamarin Forms RefreshView stopped working randomly - ios

Can someone please tell me what I'm doing wrong? The RefreshView was just working yesterday and today I can't get it to work in any page. I created a brand new page with just a RefreshView and when I try to pull down it doesn't budge. Doesn't pull down, doesn't refresh, nothing. It was just working last night and today after no code changes it's not working. I've tried on the simulator and on my actual iPad. Before anyone suggests, there are no updates to any of my NuGet packages and I can't find any reference to this issue on Google.
XF: v5.0.0.2244
View:
<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage
x:Class="MyApp.UI.Pages.PricingPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:MyApp.Core.ViewModels;assembly=MyApp.Core"
xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
Title="{Binding Title}"
x:DataType="viewModels:PricingViewModel"
x:TypeArguments="viewModels:PricingViewModel">
<views:MvxContentPage.Content>
<RefreshView Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}">
<CollectionView ItemsSource="{Binding MenuItems}" />
</RefreshView>
</views:MvxContentPage.Content>
</views:MvxContentPage>
ViewModel:
using System.Threading;
using System.Threading.Tasks;
using MyApp.Core.ChurromigosApi;
using MyApp.Core.Services;
using MyApp.Core.ViewModels.Base;
using MvvmCross.Commands;
using MvvmCross.ViewModels;
namespace MyApp.Core.ViewModels
{
public class PricingViewModel : BaseViewModel
{
private readonly IMenuItemService menuItemService;
public PricingViewModel(IMenuItemService menuItemService)
{
this.menuItemService = menuItemService;
this.RefreshCommand = new MvxAsyncCommand(this.Refresh);
this.MenuItems = new MvxObservableCollection<MenuItem>();
this.Title = "Pricing";
}
public MvxObservableCollection<MenuItem> MenuItems { get; set; }
public IMvxAsyncCommand RefreshCommand { get; }
public bool IsRefreshing { get; set; }
public override Task Initialize()
{
this.IsRefreshing = true;
return Task.CompletedTask;
}
private async Task Refresh()
{
var allMenuItems = await this.menuItemService.GetMenuItems(CancellationToken.None);
this.MenuItems.Clear();
this.MenuItems.AddRange(allMenuItems);
}
}
}

I found a similar problem with RefreshView, though in my case instead of becoming unresponsive, the app just crashes.
It appears that we need to pay more attention to the binding Mode for the property used for the IsRefreshing indicator:
If we want the RefreshView to automatically trigger the command when the VM's binded property is changed to True, then don't include a Mode, or use Mode=TwoWay, but don't manually change the value or you'll be stuck in a loop (or include a if(IsRefreshing) return; in your command or you wont get any results)
If we want to manually set the VM's property in the backend to show/hide the IsRefreshing activity indicator without triggering the command, then use Mode=OneWay or else you will be stuck in a loop.
In your particular case it could be related to the fact that you're setting your IsRefreshing property to true, and then you never set it back to false? I would wrap your Refreshing() method's code with a try, setting IsRefreshing=false at the beginning and then =false in a finally block.

Related

How to dynamically add a controller in a ASP.NET Core 6 MVC application

I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.
I found some way to somewhat achieve this but not quite.
I'm able to dynamically add my controller but somehow it reflects only on the second request.
So here is what I do: first I initialize my console app as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace DynamicControllerServer
{
internal class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;
// Store thePartManager in my Middleware to be able to add controlelr after initialization is done
MyMiddleware._partManager = partManager;
// Register controller change event
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
// Add Middleware which is responsible to cactn the request and dynamically add the missing controller
app.UseMiddleware<MyMiddleware>();
app.RunAsync();
Console.WriteLine("Server has been started successfully ...");
Console.ReadLine();
}
}
}
Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;
namespace DynamicControllerServer
{
public class MyMiddleware
{
public RequestDelegate _next { get; }
private string dllName = "DynamicController1.dll";
static public ApplicationPartManager _partManager = null;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.HasValue)
{
var queryParams = httpContext.Request.Path.Value;
if(httpContext.Request.Path.Value.Contains("api/dynamic"))
{
// Dynamically load assembly
Assembly assembly = assembly = Assembly.LoadFrom(#"C:\Temp\" + dllName);
// Add controller to the application
AssemblyPart _part = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(_part);
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
await _next(httpContext); // calling next middleware
}
}
}
The ActionDescriptorChange provider looks like this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace DynamicControllerServer
{
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
Dynamic controller is in separate dll and is very simple:
using Microsoft.AspNetCore.Mvc;
namespace DotNotSelfHostedOwin
{
[Route("api/[controller]")]
[ApiController]
public class DynamicController : ControllerBase
{
public string[] Get()
{
return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
}
}
}
Here are the packages used in that project:
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
This works "almost" fine ... when first request is made to:
https://localhost:5001/api/dynamic
then it goes in the middleware and load the assembly, but returns a 404 error.
Then second request will actually work as expected:
Second request returns the expected result:
I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.
Question is: what should be the proper way to achieve this?
Second question I have is say now the external dll holding our dynamic controller is updated.
How can I reload that controller to get the new definition?
Any help would be appreciated
Thanks in advance
Nick
Here is the answer to my own question in case it can help somebody out there.
It seems building and loading the controller from the middleware will always end up with failure on the first call.
This makes sense since we are already in the http pipeline.
I end up doing same thing from outside the middleware.
Basically my application detect a change in the controller assembly, unload the original assembly and load the new one.
You cannot use the Default context since it will not allow reloading different dll for same assembly:
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates
To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:
public class MyOwnContext: AssemblyLoadContext
{
// You can find lots of example in the net
}
When you want to unload the assembly, you just unload the context:
MyOwnContextObj.Unload();
Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart.
To add controller
ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);
To remove:
_PartManager.ApplicationParts.Remove(part);
On course once done, still use following piece of code to acknowledge the change:
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
That allow updating controller on the fly with no interruption of service.
Hope this helps people out there.
I have done a similar solution (used for managing a web app plugins) with some differences that may help you:
List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup
Instead of registering controllers when they are called, register them at program.cs/start up :
//Foreah dllName from settings file
var assembly = Assembly.LoadFrom(#"Base address" + dllNameLoadedFromSettings);
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
// Any other configuration based on the usage you want
Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.

UWP Template 10 and Service Dendency Injection (MVVM) not WPF

I have spent over two weeks searching google, bing, stack overflow, and msdn docs trying to figure out how to do a proper dependency injection for a mobile app that I am developing. To be clear, I do DI every day in web apps. I do not need a crash course on what, who, and why DI is important. I know it is, and am always embracing it.
What I need to understand is how this works in a mobile app world, and in particular a UWP Template 10 Mobile app.
From my past, in a .net/Asp app I can "RegisterType(new XYZ).Singleton() blah" {please forgive syntax; just an example} in App_Start.ConfigureServices. This works almost identical in .netcore, granted some syntactic changes.
My problem is now I am trying to provide my api is going to an UWP app that needs to digest my IXYZ service. By no means do I think that they should "new" up an instance every time. There has to be a way to inject this into a container on the UWP side; and I feel I am missing something very simple in the process.
Here is the code I have:
App.xaml.cs
public override async Task OnStartAsync(StartKind startKind, IActivatedEventArgs args)
{
// TODO: add your long-running task here
//if (args.Kind == ActivationKind.LockScreen)
//{
//}
RegisterServices();
await NavigationService.NavigateAsync(typeof(Views.SearchCompanyPage));
}
public static IServiceProvider Container { get; private set; }
private static void RegisterServices()
{
var services = new ServiceCollection();
services.AddSingleton<IXYZ, XYZ>();
Container = services.BuildServiceProvider();
}
MainPage.xaml.cs:
public MainPage()
{
InitializeComponent();
NavigationCacheMode = NavigationCacheMode.Enabled;
}
MainPageViewModel:
public class MainPageViewModel : ViewModelBase
{
private readonly IXYZ _xyz;
public MainPageViewModel(IXYZ xyz)
{
//Stuff
_xyz= xyz;
}
}
I now get the error:
XAML MainPage...ViewModel type cannot be constructed. In order to be constructed in XAML, a type cannot be abstract, interface nested generic or a struct, and must have a public default constructor.
I am willing to use any brand of IoC Container, but what I need is an example of how to properly use DI for services in a UWP app. 99.9% of questions about DI is about Views (i.e. Prism?) not just a simple DI for a service (i.e. DataRepo; aka API/DataService).
Again, I feel I am missing something obvious and need a nudge in the right direction. Can somebody show me an example project, basic code, or a base flogging on how I should not be a programmer...please don't do that (I don't know if my ego could take it).
You can try to Microsoft.Hosting.Extensions just like ASP.NET, there's an implementation on Xamarin.Forms by James Montemagno, as well it can be used in UWP I have tried and it works perfectly. You have to change some parts in order to get it working.
In OnLaunched Method add Startup.Init();
public static class Startup
{
public static IServiceProvider ServiceProvider { get; set; }
public static void Init()
{
StorageFolder LocalFolder = ApplicationData.Current.LocalFolder;
var configFile = ExtractResource("Sales.Client.appsettings.json", LocalFolder.Path);
var host = new HostBuilder()
.ConfigureHostConfiguration(c =>
{
// Tell the host configuration where to file the file (this is required for Xamarin apps)
c.AddCommandLine(new string[] { $"ContentRoot={LocalFolder.Path}" });
//read in the configuration file!
c.AddJsonFile(configFile);
})
.ConfigureServices((c, x) =>
{
// Configure our local services and access the host configuration
ConfigureServices(c, x);
}).
ConfigureLogging(l => l.AddConsole(o =>
{
//setup a console logger and disable colors since they don't have any colors in VS
o.DisableColors = true;
}))
.Build();
//Save our service provider so we can use it later.
ServiceProvider = host.Services;
}
static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
//ViewModels
services.AddTransient<HomeViewModel>();
services.AddTransient<MainPageViewModel>();
}
static string ExtractResource(string filename, string location)
{
var a = Assembly.GetExecutingAssembly();
using (var resFilestream = a.GetManifestResourceStream(filename))
{
if (resFilestream != null)
{
var full = Path.Combine(location, filename);
using (var stream = File.Create(full))
{
resFilestream.CopyTo(stream);
}
}
}
return Path.Combine(location, filename);
}
}
Injecting a ViewModel is possible as well which is pretty nice.
With help from #mvermef and the SO question Dependency Injection using Template 10 I found a solutions. This turned out to be a rabbit hole where at every turn I ran into an issue.
The first problem was just getting Dependency Injection to work. Once I was able to get that figured out from the sources above I was able to start injecting my services into ViewModels and setting them to the DataContext in the code behind.
Then I ran into an injection issue problem with injecting my IXYZ services into the ViewModels of UserControls.
Pages and their ViewModels worked great but I had issues with the DataContext of the UserControl not being injected with UserControl's ViewModel. They were instead getting injected by the Page's ViewModel that held it.
The final solution turned out to be making sure that the UserControl had the DataContext being set in XAML not the code behind, as we did with the Pages, and then creating a DependencyProperty in the code behind.
To show the basic solution read below.
To make it work I started with:
APP.XAML.CS
public override async Task OnStartAsync(StartKind startKind, IActivatedEventArgs args)
{
// long-running startup tasks go here
RegisterServices();
await Task.CompletedTask;
}
private static void RegisterServices()
{
var services = new ServiceCollection();
services.AddSingleton<IRepository, Repository>();
services.AddSingleton<IBinderService, BinderServices>();
**//ViewModels**
**////User Controls**
services.AddSingleton<AddressesControlViewModel, AddressesControlViewModel>();
services.AddSingleton<CompanyControlViewModel, CompanyControlViewModel>();
**//ViewModels**
**////Pages**
services.AddSingleton<CallListPageViewModel, CallListPageViewModel>();
services.AddSingleton<CallListResultPageViewModel, CallListResultPageViewModel>();
etc....
Container = services.BuildServiceProvider();
}
public override INavigable ResolveForPage(Page page, NavigationService navigationService)
{
**//INJECT THE VIEWMODEL FOR EACH PAGE**
**//ONLY THE PAGE NOT USERCONTROL**
if (page is CallListPage)
{
return Container.GetService<CallListPageViewModel>();
}
if (page is CallListResultPage)
{
return Container.GetService<CallListResultPageViewModel>();
}
etc...
return base.ResolveForPage(page, navigationService);
}
In the code behind for the Page
CALLLISTPAGE.XAML.CS
public CallListPage()
{
InitializeComponent();
}
CallListPageViewModel _viewModel;
public CallListPageViewModel ViewModel
{
get { return _viewModel ?? (_viewModel = (CallListPageViewModel)DataContext); }
}
In your XAML add your UserControl
CALLLISTPAGE.XAML
<binder:CompanyControl Company="{x:Bind ViewModel.SelectedCompany, Mode=TwoWay}"/>
In your UserControl make sure to add the DataContext to the XAML NOT the code behind like we did with the pages.
COMPANYCONTROL.XAML
<UserControl.DataContext>
<viewModels:CompanyControlViewModel x:Name="ViewModel" />
</UserControl.DataContext>
In the UserControl Code Behind add a Dependency Property
COMPANYCONTROL.XAML.CS
public static readonly DependencyProperty CompanyProperty = DependencyProperty.Register(
"Company", typeof(Company), typeof(CompanyControl), new PropertyMetadata(default(Company), SetCompany));
public CompanyControl()
{
InitializeComponent();
}
public Company Company
{
get => (Company) GetValue(CompanyProperty);
set => SetValue(CompanyProperty, value);
}
private static void SetCompany(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as CompanyControl;
var viewModel = control?.ViewModel;
if (viewModel != null)
viewModel.Company = (Company) e.NewValue;
}
In the end I am not sure if this is an elegant solution but it works.

Visibility ValueConverter Update Logic to MvvmCross v3

I updated an older android project from mvvmcross v2 to mvvmcross v3.
Got one more problem now.
The visibility doesn't work, its doing nothing.
Old solution looked like this (worked fine):
In Setup.cs
protected override IEnumerable<Type> ValueConverterHolders
{
get { return new[] { typeof(Converters) }; }
}
Converters.cs
using Cirrious.MvvmCross.Converters.Visibility;
namespace Test.Droid
{
public class Converters
{
public readonly MvxVisibilityConverter Visibility = new MvxVisibilityConverter();
}
}
Any .axml (change visibility of LinearLayout):
<LinearLayout style="#style/LinearLayoutSmall" local:MvxBind="{'Visibility':{'Path':'TestIsVisible','Converter':'Visibility'}}">
New solution (doesn't work):
In Setup.cs
protected override List<Type> ValueConverterHolders
{
get { return new List<Type> { typeof(Converters) }; }
}
Converters.cs
using Cirrious.MvvmCross.Plugins.Visibility;
namespace Test.Droid
{
public class Converters
{
public readonly MvxVisibilityValueConverter Visibility = new MvxVisibilityValueConverter();
}
}
Any .axml
<LinearLayout style="#style/LinearLayoutSmall" local:MvxBind="Visibility TestIsVisible, Converter=Visibility">
There's probably a problem with the swissbinding syntax or I'm using false classes?
Any help appreciated!
UPDATE
I forgot these lines:
public override void LoadPlugins(IMvxPluginManager pluginManager)
{
pluginManager.EnsurePluginLoaded<PluginLoader>();
pluginManager.EnsurePluginLoaded<Cirrious.MvvmCross.Plugins.Visibility.PluginLoader>();
base.LoadPlugins(pluginManager);
}
I guess its necessary but now I'm having following error:
(from the MvxPluginManager Class)...
I checked all references and the dll/project *.Visibility.Droid.dll is referenced in my mainproject and everywhere else...
Without running and debugging a complete sample of your code I can't see what the problem is. One guess is that it could be in the plugin setup for visibility, but that is only a guess. The debug trace for your app might reveal some information on this.
Alternatively, it might be easier to simply try setting up a new project and getting visibility working in that, then comparing that code back to your existing app.
Value Converters in v3 are documented in https://github.com/MvvmCross/MvvmCross/wiki/Value-Converters.
The preferred way of referencing them is simply to let MvvmCross find them by reflection - see the section on https://github.com/MvvmCross/MvvmCross/wiki/Value-Converters#referencing-value-converters-in-touch-and-droid
A sample app, including visibility, is in: https://github.com/MvvmCross/MvvmCross-Tutorials/tree/master/ValueConversion - e.g. https://github.com/MvvmCross/MvvmCross-Tutorials/blob/master/ValueConversion/ValueConversion.UI.Droid/Resources/Layout/View_Visibility.axml

MVVM Light - Unable to update parent view from child - nested edit

My situation is slightly different than from other posts and I was not able to solve it with the other trhreads. So that why I ask.
I have a class that is obtained from deserializing an XML like:
<?xml version="1.0" encoding="UTF-8"?>
<node>
<leaf>
<name>node 1</name>
<text>text 1</text>
<url>url 1</url>
</leaf>
<leaf>
<name>node 2</name>
<text>text 2</text>
<url>url 2</url>
</leaf>
</node>
so the class is:
[XmlRoot("node")]
public class csNodeList
{
public csNodeList()
{
Leaf = new csLeafCollection();
}
[XmlElement("leaf")]
public csLeafCollection Leaf
{
get;
set;
}
}
public class csLeaf
{
public csLeaf()
{
Name ="";
Description = "";
Address = "";
}
[XmlElement("name")]
public string Name
{
get;
set;
}
[XmlElement("text")]
public string Description
{
get;
set;
}
[XmlElement("url")]
public string Address
{
get;
set;
}
}
public class csLeafCollection : System.Collections.ObjectModel.ObservableCollection<csLeaf>
{
}
Then I have 2 Views, one to show all the leafs and one to edit one leaf. I've implemented commit and rollback so I use messaging back and forth to pass the new values and I store the old ones.
To do so I copy the objects a a backup variable and then I modify the ones associated via binding to the XAML view, in this way (in theory) any change to the ViewModel data should be reflected.
Also is better because if I commit the changes I just discard the backup variables (this is 90% of the times) and if I need to roll back I copy back from the backup variables.
MainView:
public const string listPropertyName = "list";
private csNodeList _list = new csNodeList();
public csNodeList list
{
get
{
return _list;
}
set
{
Set(listPropertyName, ref _list, value, false);
}
}
Using the message I send back the new values of a node and I put them in the correct position:
private void DoSomething(csMessage message)
{
csMessage rmessage;
if (message != null)
{
switch (message.destination)
{
case csMessage.e2MessageDest.updateNode:
//_editP should be fine.
list.Leaf[list.Leaf.IndexOf(_editP)].Name = ((csLeaf)message.payload).Name;
list.Leaf[list.Leaf.IndexOf(_editP)].Text= ((csLeaf)message.payload).Text;
list.Leaf[list.Leaf.IndexOf(_editP)].Address = ((csLeaf)message.payload).Address;
RaisePropertyChanged(listPropertyName , null, _list, true);
break;
}
}
}
The code is executed correctly and the item is changed.
BUT the RaisePropertyChanged is ignored. I've tried even just the one with the listPropertyName without any change.
If I save the changes exit from the app and get back I see the new value correctly stored
Can you please help me?
Thanks,
Massimo
The reason why your RaisePropertyChanged is ignored is hat yor Leaf class des not implement INotifyOropertyChanged. Commonly the model is wrapped into a view model which then implements INotifyPropertyChanged to notify the view hat something has happened.
However, you also can implement INotifyPropertyChanged on the model class directly. To implement INotifyPropertyChanged each property has to raise the propty changed event.
public string Property {
get { ... }
set {
if (_propertyField == value)
return;
_propertyField = value;
RaisePropertyChanged("Property");
}
}
The code assumes hat there is a method RaisePropertyChanged which actually taises the PropertyChangedEvent.
Thank you everyone for the help.
Investigating your suggestion I've found a slightly different solution; as you correctly said the issue is that the leaf fields are not "observable" so they do not generate a notification event.
I've noticed that if I add or Delete a profile the binding is updated.
So what I've decided to do is not to edit directly the leafs but to replace the node.
What I do not like is that I have to create a node to replace the old one and this allocates a little bit more memory... but for small data like the one I have it can work without any major impact on the app performance/memory foot print.
Here is what I do:
csLeaf _leaf = new slLeaf();
_leaf.Name = ((csLeaf)message.payload).Name;
_leaf.Text= ((csLeaf)message.payload).Text;
_leaf.URL = ((csLeaf)message.payload).Address;
list.Leaf[list.Leaf.IndexOf(_editP)] = _leaf;
To optimized readabilty of code I've enhanced it adding a constructor with 3 parameters so that the code can be:
csLeaf _leaf = new slLeaf(((csLeaf)message.payload).Name, ((csLeaf)message.payload).Text, ((csLeaf)message.payload).Address);
list.Leaf[list.Leaf.IndexOf(_editP)] = _leaf;
The constructor is:
public csLeaf(string _name, string _description, string _address)
{
Name = _name;
Description = _description;
Address = _address;
}

TFS Activity for InvokeProcess (without wait)

I see lots of posts for waiting on the return value of various activities, but in this case, I'd like to kick off an activity and then not wait. I just want to copy a directory over a (very) slow network, so I'd prefer not to create another activity or use a batch script for this. Anyone done this? Is there a clean way? I could cobble something together, but I'm trying to keep this as vanilla as possible.
I don't think that something out of the box is available for you to use. One way to proceed is to organize an "Invoke Process" that invokes another service that does the actual copying. So from within Build you advance and let the invoked entity (that is out of scope in terms of TFS build) do the actual activity. This does come with certain deficiencies, the more important being that you won't ever know in your build-logs if this succeeded or failed.Another option is to use the Parallel activity (it's in the Toolbox under "Control Flow" - System.Activities.Statements.Parallel). This is not quite like what you need (kick & forget), still it could allow you to do other stuff while your copy happens.
Here is simple custom activity that will create new process:
[BuildActivity (HostEnvironmentOption.All)]
public sealed class InvokeProcessAsync : CodeActivity
{
[RequiredArgument]
public InArgument<string> FileName { get; set; }
public InArgument<string> Arguments { get; set; }
public InArgument<string> WorkingDirectory { get; set; }
public InArgument<IDictionary<string, string>> EnvironmentVariables { get; set; }
protected override void Execute (CodeActivityContext context)
{
context.DublicateOperationsLogsToBuildOutput ();
var psi = new ProcessStartInfo
{
FileName = context.GetValue (this.FileName),
Arguments = context.GetValue (this.Arguments),
WorkingDirectory = context.GetValue (this.WorkingDirectory)
};
var env_vars = context.GetValue (this.EnvironmentVariables);
if (env_vars != null)
{
foreach (var v in env_vars)
psi.EnvironmentVariables.Add (v.Key, v.Value);
}
Process.Start (psi);
}
}
CopyDirectory activity from the Build.Workflow assembly is what you need.

Resources