Did you mean to add the backslash in the tile template? It should just be forward slashes. Also the doc states you need to use the level row and column parameter instead of x y z. That might be why. (If you load the layer, you should get a load error telling you exactly this) Also you don't need the subdomain overload, since you don't have a subdomain in the template uri.
So you'd have something like:
private readonly string _templateUri = "file:///C:/Users/Zach/Desktop/DEM/XYZ2/{level}/{col}/{row}.png";
This made it work for me. You can also instead implement a custom ImageTiledLayer, which gives you full control of how the image bytes are loaded. It is more work, but gives a lot better control. Example:
using Esri.ArcGISRuntime.ArcGISServices;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Mapping;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace CustomTileLayerSample
{
public class LocalTiledLayer : ImageTiledLayer
{
private readonly string _templateUri = "C:\\Users\\Zach\\Desktop\\DEM\\XYZ2\\{0}\\{1}\\{2}.png";
public LocalTiledLayer() : base(CreateTileInfo(), new Envelope(-2.003750722959434E7, -1.997186888040859E7, 2.003750722959434E7, 1.9971868880408563E7, SpatialReferences.WebMercator))
{
}
private static TileInfo CreateTileInfo()
{
List<LevelOfDetail> levels = new List<LevelOfDetail>();
var resolution = 156543.03392800014;
var scale = 5.91657527591555E8;
int levelCount = 24;
for (int i = 0; i < levelCount; i++)
{
levels.Add(new LevelOfDetail(i, resolution, scale));
resolution /= 2; scale/=2;
}
return new TileInfo(96, TileImageFormat.Png32, levels, new MapPoint(-2.0037508342787E7, 2.0037508342787E7, SpatialReferences.WebMercator), SpatialReferences.WebMercator, 256, 256);
}
protected override async Task<ImageTileData> GetTileDataAsync(int level, int row, int column, CancellationToken cancellationToken)
{
var file = string.Format(_templateUri, level, row, column);
if (File.Exists(file))
{
var bytes = await File.ReadAllBytesAsync(file).ConfigureAwait(false);
return new ImageTileData(level, row, column, bytes, "image/png");
}
return new ImageTileData(level, row, column, new byte[] { }, "image/png");
}
}
}
You could even take this a step further and generate tiles on the fly:
protected override Task<ImageTileData> GetTileDataAsync(int level, int row, int column, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
using var pen = new System.Drawing.Pen(System.Drawing.Color.Black);
using var font = new System.Drawing.Font("Times New Roman", 12);
using var image = new System.Drawing.Bitmap(256, 256);
using var g = Graphics.FromImage(image);
g.DrawString($"{level}/{row}/{column}", font, System.Drawing.Brushes.Black, new PointF(100, 122));
g.DrawLine(pen, 0, 0, 255, 0);
g.DrawLine(pen, 0, 0, 0, 255);
using var filestream = new MemoryStream();
image.Save(filestream, System.Drawing.Imaging.ImageFormat.Png);
return new ImageTileData(level, row, column, filestream.ToArray(), "image/png");
});
}