Skip to content
94 changes: 94 additions & 0 deletions .github/copilot-instructions/templates.yml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed path-specific custom instructions format following GitHub documentation. Changes:

  • Renamed from templates.yml to templates.md
  • Added YAML frontmatter with applies_to field
  • Format now matches GitHub's documented structure

The file now uses the correct format:

---
applies_to:
  - src/Templates/**
---

Commit 25df86b (will be new commit after push).

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copilot instructions for working with .NET MAUI Templates
# This file provides specific guidance when modifying template files in src/Templates/

# Apply these instructions only to files in the Templates directory
applicability:
paths:
- 'src/Templates/**'

instructions: |
# Working with .NET MAUI Templates

When modifying files in the `src/Templates/` directory, you must follow special template semantics and conventions to ensure the templates work correctly when users create new projects.

## Template Conditional Compilation Directives

Templates use special comment markers to control how preprocessor directives are processed during template instantiation:

### Platform-Specific Directives (Build-Time)

Platform-specific `#if` directives (like `#if WINDOWS`, `#if ANDROID`, `#if IOS`, `#if MACCATALYST`) must be wrapped with `//-:cnd:noEmit` and `//+:cnd:noEmit` markers:

```csharp
//-:cnd:noEmit
#if WINDOWS
// Windows-specific code
#endif
//+:cnd:noEmit
```

**Why?** These markers tell the template engine to preserve these directives in the generated code exactly as-is, so they will be evaluated at compile-time when the user builds their project.

**Examples:**
```csharp
//-:cnd:noEmit
#if DEBUG
builder.Logging.AddDebug();
#endif
//+:cnd:noEmit

//-:cnd:noEmit
#if IOS || MACCATALYST
handlers.AddHandler<CollectionView, CollectionViewHandler2>();
#endif
//+:cnd:noEmit

//-:cnd:noEmit
#if WINDOWS
Microsoft.Maui.Controls.Handlers.Items.CollectionViewHandler.Mapper.AppendToMapping(
"KeyboardAccessibleCollectionView",
(handler, view) => { /* ... */ });
#endif
//+:cnd:noEmit
```

### Template Parameter Directives (Template-Time)

Template parameter directives (like `#if (IncludeSampleContent)`) do NOT use the `//-:cnd:noEmit` markers:

```csharp
#if (IncludeSampleContent)
using CommunityToolkit.Maui;
#endif
```

**Why?** These directives are evaluated when the template is instantiated (when user runs `dotnet new maui`), not when the code is compiled.

## Template Naming Conventions

- Template project names use placeholders like `MauiApp._1` which get replaced with the user's actual project name
- Namespaces follow the same pattern: `namespace MauiApp._1;`
- These will be transformed to the user's chosen project name during template instantiation

## Files to Exclude from Template Changes

Never modify auto-generated files in templates:
- `cgmanifest.json` - Auto-generated component governance manifest
- `templatestrings.json` - Auto-generated localization file

These files are regenerated during the build process and should not be manually edited.

## Template Testing

When making changes to templates:
1. Build the template project: `dotnet build src/Templates/src/Microsoft.Maui.Templates.csproj`
2. For comprehensive testing, use the `build.ps1` script in the Templates directory to pack, install, and test the template
3. Verify the generated project compiles for all target platforms

## Quick Reference

| Directive Type | Wrapper Needed | Example |
|---|---|---|
| Platform-specific (`#if WINDOWS`, `#if ANDROID`, etc.) | ✅ Yes - use `//-:cnd:noEmit` | Build-time platform detection |
| Debug mode (`#if DEBUG`) | ✅ Yes - use `//-:cnd:noEmit` | Build configuration |
| Template parameters (`#if (IncludeSampleContent)`) | ❌ No | Template instantiation options |
4 changes: 2 additions & 2 deletions src/Templates/src/templates/maui-mobile/AppShell.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
SegmentWidth="40" SegmentHeight="40">
<sf:SfSegmentedControl.ItemsSource>
<x:Array Type="{x:Type sf:SfSegmentItem}">
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}"/>
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}"/>
<sf:SfSegmentItem ImageSource="{StaticResource IconLight}" SemanticProperties.Description="Light mode"/>
<sf:SfSegmentItem ImageSource="{StaticResource IconDark}" SemanticProperties.Description="Dark mode"/>
</x:Array>
</sf:SfSegmentedControl.ItemsSource>
</sf:SfSegmentedControl>
Expand Down
3 changes: 3 additions & 0 deletions src/Templates/src/templates/maui-mobile/AppShell.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public AppShell()
#if (IncludeSampleContent)
var currentTheme = Application.Current!.RequestedTheme;
ThemeSegmentedControl.SelectedIndex = currentTheme == AppTheme.Light ? 0 : 1;
#endif
#if ANDROID || WINDOWS
SemanticProperties.SetDescription(ThemeSegmentedControl, "Theme selection");
#endif
}
#if (IncludeSampleContent)
Expand Down
32 changes: 32 additions & 0 deletions src/Templates/src/templates/maui-mobile/Data/TagRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ public async Task<int> SaveItemAsync(Tag item, int projectID)
await Init();
await SaveItemAsync(item);

var isAssociated = await IsAssociated(item, projectID);
if (isAssociated)
{
return 0; // No need to save again if already associated
}

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

Expand All @@ -212,6 +218,32 @@ public async Task<int> SaveItemAsync(Tag item, int projectID)
return await saveCmd.ExecuteNonQueryAsync();
}

/// <summary>
/// Checks if a tag is already associated with a specific project.
/// </summary>
/// <param name="item">The tag to save.</param>
/// <param name="projectID">The ID of the project.</param>
/// <returns>If tag is already associated with this project</returns>
async Task<bool> IsAssociated(Tag item, int projectID)
{
await Init();

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

// First check if the association already exists
var checkCmd = connection.CreateCommand();
checkCmd.CommandText = @"
SELECT COUNT(*) FROM ProjectsTags
WHERE ProjectID = @projectID AND TagID = @tagID";
checkCmd.Parameters.AddWithValue("@projectID", projectID);
checkCmd.Parameters.AddWithValue("@tagID", item.ID);

int existingCount = Convert.ToInt32(await checkCmd.ExecuteScalarAsync());

return existingCount != 0;
}

/// <summary>
/// Deletes a tag from the database.
/// </summary>
Expand Down
19 changes: 15 additions & 4 deletions src/Templates/src/templates/maui-mobile/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ public static MauiApp CreateMauiApp()
#if (IncludeSampleContent)
.UseMauiCommunityToolkit()
.ConfigureSyncfusionToolkit()
//-:cnd:noEmit
.ConfigureMauiHandlers(handlers =>
{
//-:cnd:noEmit
#if IOS || MACCATALYST
handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();
#if WINDOWS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot for templates can you please make sure to consult template semantics?

For example, these if/defs need to be included into the template code so they need to be surrounded by

//-:cnd:noEmit
#if windows
#endif
//+:cnd:noEmit

Can you please add a copilot instruction that's specific to the templates area location that will ensure you use all the proper rules in the future for updating templates?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the template semantics in MauiProgram.cs by adding //-:cnd:noEmit and //+:cnd:noEmit markers around the #if WINDOWS directive (lines 24-39).

Also added a comprehensive "Working with Templates" section to .github/copilot-instructions.md that documents:

  • When to use //-:cnd:noEmit markers (platform-specific directives like #if WINDOWS, #if DEBUG)
  • When NOT to use them (template parameters like #if (IncludeSampleContent))
  • Template naming conventions
  • Files to exclude from template changes
  • Template testing guidelines
  • Quick reference table

Changes verified in commit b1e5115 (will be the new commit after this push).

Microsoft.Maui.Controls.Handlers.Items.CollectionViewHandler.Mapper.AppendToMapping("KeyboardAccessibleCollectionView", (handler, view) =>
{
handler.PlatformView.SingleSelectionFollowsFocus = false;
});

Microsoft.Maui.Handlers.ContentViewHandler.Mapper.AppendToMapping(nameof(Pages.Controls.CategoryChart), (handler, view) =>
{
if (view is Pages.Controls.CategoryChart && handler.PlatformView is ContentPanel contentPanel)
{
contentPanel.IsTabStop = true;
}
});
#endif
})
//+:cnd:noEmit
})
#endif
.ConfigureFonts(fonts =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public partial class MainPageModel : ObservableObject, IProjectTaskPageModel
[ObservableProperty]
private string _today = DateTime.Now.ToString("dddd, MMM d");

[ObservableProperty]
private Project? selectedProject;

public bool HasCompletedTasks
=> Tasks?.Any(t => t.IsCompleted) ?? false;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiApp._1.Models;
using System.Collections.ObjectModel;
using System.Windows.Input;

namespace MauiApp._1.PageModels;

Expand Down Expand Up @@ -34,6 +36,8 @@ public partial class ProjectDetailPageModel : ObservableObject, IQueryAttributab
[ObservableProperty]
private List<Tag> _allTags = [];

public IList<object> SelectedTags { get; set; } = new List<object>();

[ObservableProperty]
private IconData _icon;

Expand Down Expand Up @@ -135,6 +139,10 @@ private async Task LoadData(int id)
foreach (var tag in allTags)
{
tag.IsSelected = _project.Tags.Any(t => t.ID == tag.ID);
if (tag.IsSelected)
{
SelectedTags.Add(tag);
}
}
AllTags = new(allTags);
}
Expand All @@ -156,7 +164,6 @@ private async Task TaskCompleted(ProjectTask task)
OnPropertyChanged(nameof(HasCompletedTasks));
}


[RelayCommand]
private async Task Save()
{
Expand All @@ -174,14 +181,11 @@ private async Task Save()
_project.Icon = Icon.Icon ?? FluentUI.ribbon_24_regular;
await _projectRepository.SaveItemAsync(_project);

if (_project.IsNullOrNew())
foreach (var tag in AllTags)
{
foreach (var tag in AllTags)
if (tag.IsSelected)
{
if (tag.IsSelected)
{
await _tagRepository.SaveItemAsync(tag, _project.ID);
}
await _tagRepository.SaveItemAsync(tag, _project.ID);
}
}

Expand Down Expand Up @@ -236,7 +240,7 @@ private Task NavigateToTask(ProjectTask task) =>
Shell.Current.GoToAsync($"task?id={task.ID}");

[RelayCommand]
private async Task ToggleTag(Tag tag)
internal async Task ToggleTag(Tag tag)
{
tag.IsSelected = !tag.IsSelected;

Expand All @@ -253,6 +257,7 @@ private async Task ToggleTag(Tag tag)
}

AllTags = new(AllTags);
SemanticScreenReader.Announce($"{tag.Title} {(tag.IsSelected ? "selected" : "unselected")}");
}

[RelayCommand]
Expand All @@ -269,4 +274,34 @@ private async Task CleanTasks()
OnPropertyChanged(nameof(HasCompletedTasks));
await AppShell.DisplayToastAsync("All cleaned up!");
}

[RelayCommand]
private async Task SelectionChanged(object parameter)
{
if (parameter is IEnumerable<object> enumerableParameter)
{
var currentSelection = enumerableParameter.OfType<Tag>().ToList();
var previousSelection = AllTags.Where(t => t.IsSelected).ToList();

// Handle newly selected tags
foreach (var tag in currentSelection.Except(previousSelection))
{
tag.IsSelected = true;
if (!_project.IsNullOrNew())
{
await _tagRepository.SaveItemAsync(tag, _project.ID);
}
}

// Handle deselected tags
foreach (var tag in previousSelection.Except(currentSelection))
{
tag.IsSelected = false;
if (!_project.IsNullOrNew())
{
await _tagRepository.DeleteItemAsync(tag, _project.ID);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#nullable disable
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiApp._1.Data;
Expand All @@ -14,6 +13,9 @@ public partial class ProjectListPageModel : ObservableObject
[ObservableProperty]
private List<Project> _projects = [];

[ObservableProperty]
private Project? selectedProject;

public ProjectListPageModel(ProjectRepository projectRepository)
{
_projectRepository = projectRepository;
Expand All @@ -26,8 +28,8 @@ private async Task Appearing()
}

[RelayCommand]
Task NavigateToProject(Project project)
=> Shell.Current.GoToAsync($"project?id={project.ID}");
Task? NavigateToProject(Project project)
=> project is null ? Task.CompletedTask : Shell.Current.GoToAsync($"project?id={project.ID}");

[RelayCommand]
async Task AddProject()
Expand Down
Loading